1){ out.push("', actions.between, '"); } \n');
+ }
+ },
+ doForEach: function(action, actions) {
+ var me = this,
+ L = me.level,
+ up = L - 1,
+ s, parentAssignment;
+ // If it's just a propName, use it directly in the switch
+ if (action === '.') {
+ s = 'values';
+ } else if (me.propNameRe.test(action)) {
+ s = me.parseTag(action);
+ } else // Otherwise, it must be an expression, and needs to be returned from an fn
+ // which uses with(values)
+ {
+ s = me.addFn(action) + me.callFn;
+ }
+ /*
+ We are trying to produce a block of code that looks like below. We use the nesting
+ level to uniquely name the control variables.
+
+ // Omit "var " if we have already been through level 2
+ var i2 = -1,
+ n2 = 0,
+ c2 = values['propName'], // c2 is the context object for the for loop
+ a2 = Array.isArray(c2);
+ r2 = values, // r2 is the values object
+ p2, // p2 is the parent context (of the outer for loop)
+ k2; // k2 is the object key while looping
+
+ // If iterating over the current data, the parent is always set to c2
+ p2 = parent = c2;
+ // If iterating over a property in an object, set the parent to the object
+ p2 = parent = a1 ? c1[i1] : c1 // set parent
+
+ for(k2 in c2){
+ xindex = ++i + 1; // xindex is 1-based
+ xkey = k2;
+ values = c2[k2]; // values is the property value
+
+ The body of the loop is whatever comes between the tpl and /tpl statements (which
+ is handled by doEnd).
+ */
+ // Declare the vars for a particular level only if we have not already declared them.
+ if (me.maxLevel < L) {
+ me.maxLevel = L;
+ me.body.push('var ');
+ }
+ if (action === '.') {
+ parentAssignment = 'c' + L;
+ } else {
+ parentAssignment = 'a' + up + '?c' + up + '[i' + up + ']:c' + up;
+ }
+ me.body.push('i', L, '=-1,n', L, '=0,c', L, '=', s, ',a', L, '=', me.createArrayTest(L), ',r', L, '=values,p', L, ',k', L, ';\n', 'p', L, '=parent=', parentAssignment, '\n', 'for(k', L, ' in c', L, '){\n', 'xindex=++i', L, '+1;\n', 'xkey=k', L, ';\n', 'values=c', L, '[k', L, '];');
+ if (actions.propName) {
+ me.body.push('.', actions.propName);
+ }
+ if (actions.between) {
+ me.body.push('if(xindex>1){ out.push("', actions.between, '"); } \n');
+ }
+ },
+ createArrayTest: ('isArray' in Array) ? function(L) {
+ return 'Array.isArray(c' + L + ')';
+ } : function(L) {
+ return 'ts.call(c' + L + ')==="[object Array]"';
+ },
+ doExec: function(action, actions) {
+ var me = this,
+ name = 'f' + me.definitions.length,
+ guards = me.guards[me.strict ? 0 : 1];
+ me.definitions.push('function ' + name + '(' + me.fnArgs + ') {', guards.doTry, ' var $v = values; with($v) {', ' ' + action, ' }', guards.doCatch, '}');
+ me.body.push(name + me.callFn + '\n');
+ },
+ //-----------------------------------
+ // Internal
+ guards: [
+ {
+ doTry: '',
+ doCatch: ''
+ },
+ {
+ doTry: 'try { ',
+ doCatch: ' } catch(e) {\n' + 'Ext.log.warn("XTemplate evaluation exception: " + e.message);\n' + '}'
+ }
+ ],
+ addFn: function(body) {
+ var me = this,
+ name = 'f' + me.definitions.length,
+ guards = me.guards[me.strict ? 0 : 1];
+ if (body === '.') {
+ me.definitions.push('function ' + name + '(' + me.fnArgs + ') {', ' return values', '}');
+ } else if (body === '..') {
+ me.definitions.push('function ' + name + '(' + me.fnArgs + ') {', ' return parent', '}');
+ } else {
+ me.definitions.push('function ' + name + '(' + me.fnArgs + ') {', guards.doTry, ' var $v = values; with($v) {', ' return(' + body + ')', ' }', guards.doCatch, '}');
+ }
+ return name;
+ },
+ parseTag: function(tag) {
+ var me = this,
+ m = me.tagRe.exec(tag),
+ name, format, args, math, v;
+ if (!m) {
+ return null;
+ }
+ name = m[1];
+ format = m[2];
+ args = m[3];
+ math = m[4];
+ // name = "." - Just use the values object.
+ if (name === '.') {
+ // filter to not include arrays/objects/nulls
+ if (!me.validTypes) {
+ me.definitions.push('var validTypes={string:1,number:1,boolean:1};');
+ me.validTypes = true;
+ }
+ v = 'validTypes[typeof values] || ts.call(values) === "[object Date]" ? values : ""';
+ }
+ // name = "#" - Use the xindex
+ else if (name === '#') {
+ v = 'xindex';
+ }
+ // name = "$" - Use the xkey
+ else if (name === '$') {
+ v = 'xkey';
+ } else if (name.substr(0, 7) === "parent.") {
+ v = name;
+ }
+ // compound Javascript property name (e.g., "foo.bar")
+ else if (isNaN(name) && name.indexOf('-') === -1 && name.indexOf('.') !== -1) {
+ v = "values." + name;
+ } else // number or a '-' in it or a single word (maybe a keyword): use array notation
+ // (http://jsperf.com/string-property-access/4)
+ {
+ v = "values['" + name + "']";
+ }
+ if (math) {
+ v = '(' + v + math + ')';
+ }
+ if (format && me.useFormat) {
+ args = args ? ',' + args : "";
+ if (format.substr(0, 5) !== "this.") {
+ format = "fm." + format + '(';
+ } else {
+ format += '(';
+ }
+ } else {
+ return v;
+ }
+ return format + v + args + ')';
+ },
+ /**
+ * @private
+ */
+ evalTpl: function($) {
+ // We have to use eval to realize the code block and capture the inner func we also
+ // don't want a deep scope chain. We only do this in Firefox and it is also unhappy
+ // with eval containing a return statement, so instead we assign to "$" and return
+ // that. Because we use "eval", we are automatically sandboxed properly.
+ eval($);
+ return $;
+ },
+ newLineRe: /\r\n|\r|\n/g,
+ aposRe: /[']/g,
+ intRe: /^\s*(\d+)\s*$/,
+ // eslint-disable-next-line no-useless-escape
+ tagRe: /^([\w-\.\#\$]+)(?:\:([\w\.]*)(?:\((.*?)?\))?)?(\s?[\+\-\*\/]\s?[\d\.\+\-\*\/\(\)]+)?$/
+}, function() {
+ var proto = this.prototype;
+ proto.fnArgs = 'out,values,parent,xindex,xcount,xkey';
+ proto.callFn = '.call(this,' + proto.fnArgs + ')';
+});
+
+/**
+ * A template class that supports advanced functionality like:
+ *
+ * - Auto-filling arrays using templates and sub-templates
+ * - Conditional processing with basic comparison operators
+ * - Basic math function support
+ * - Execute arbitrary inline code with special built-in template variables
+ * - Custom member functions
+ * - Many special tags and built-in operators that aren't defined as part of the API, but are
+ * supported in the templates that can be created
+ *
+ * XTemplate provides the templating mechanism built into {@link Ext.view.View}.
+ *
+ * The {@link Ext.Template} describes the acceptable parameters to pass to the constructor.
+ * The following examples demonstrate all of the supported features.
+ *
+ * # Sample Data
+ *
+ * This is the data object used for reference in each code example:
+ *
+ * var data = {
+ * name: 'Don Griffin',
+ * title: 'Senior Technomage',
+ * company: 'Sencha Inc.',
+ * drinks: ['Coffee', 'Water', 'More Coffee'],
+ * kids: [
+ * { name: 'Aubrey', age: 17 },
+ * { name: 'Joshua', age: 13 },
+ * { name: 'Cale', age: 10 },
+ * { name: 'Nikol', age: 5 },
+ * { name: 'Solomon', age: 0 }
+ * ]
+ * };
+ *
+ * # Auto filling of arrays
+ *
+ * The **tpl** tag and the **for** operator are used to process the provided data object:
+ *
+ * - If the value specified in for is an array, it will auto-fill, repeating the template block
+ * inside the tpl tag for each item in the array.
+ * - If for="." is specified, the data object provided is examined.
+ * - If between="..." is specified, the provided value will be inserted between the items.
+ * This is also supported in the "foreach" looping template.
+ * - While processing an array, the special variable {#} will provide the current array index + 1
+ * (starts at 1, not 0).
+ *
+ * Examples:
+ *
+ * // loop through array at root node
+ * ...
+ *
+ * // loop through array at foo node
+ * ...
+ *
+ * // loop through array at foo.bar node
+ * ...
+ *
+ * // loop through array at root node and insert ',' between each item
+ * ...
+ *
+ * Using the sample data above:
+ *
+ * var tpl = new Ext.XTemplate(
+ * 'Kids: ',
+ * '', // process the data.kids node
+ * '{#}. {name}
', // use current array index to autonumber
+ * '
'
+ * );
+ * tpl.overwrite(panel.body, data.kids); // pass the kids property of the data object
+ *
+ * An example illustrating how the **for** property can be leveraged to access specified members
+ * of the provided data object to populate the template:
+ *
+ * var tpl = new Ext.XTemplate(
+ * 'Name: {name}
',
+ * 'Title: {title}
',
+ * 'Company: {company}
',
+ * 'Kids: ',
+ * '', // interrogate the kids property within the data
+ * '{name}
',
+ * '
'
+ * );
+ * tpl.overwrite(panel.body, data); // pass the root node of the data object
+ *
+ * Flat arrays that contain values (and not objects) can be auto-rendered using the special
+ * **`{.}`** variable inside a loop. This variable will represent the value of the array
+ * at the current index:
+ *
+ * var tpl = new Ext.XTemplate(
+ * '{name}\'s favorite beverages:
',
+ * '',
+ * ' - {.}
',
+ * ' '
+ * );
+ * tpl.overwrite(panel.body, data);
+ *
+ * When processing a sub-template, for example while looping through a child array, you can access
+ * the parent object's members via the **parent** object:
+ *
+ * var tpl = new Ext.XTemplate(
+ * 'Name: {name}
',
+ * 'Kids: ',
+ * '',
+ * '',
+ * '{name}
',
+ * 'Dad: {parent.name}
',
+ * ' ',
+ * '
'
+ * );
+ * tpl.overwrite(panel.body, data);
+ *
+ * The **foreach** operator is used to loop over an object's properties. The following
+ * example demonstrates looping over the main data object's properties:
+ *
+ * var tpl = new Ext.XTemplate(
+ * '',
+ * '',
+ * // the special **`{$}`** variable contains the property name
+ * '{$} ',
+ * // within the loop, the **`{.}`** variable is set to the property value
+ * '{.} ',
+ * ' ',
+ * ' '
+ * );
+ * tpl.overwrite(panel.body, data);
+ *
+ * # Conditional processing with basic comparison operators
+ *
+ * The **tpl** tag and the **if** operator are used to provide conditional checks for deciding
+ * whether or not to render specific parts of the template.
+ *
+ * Using the sample data above:
+ *
+ * var tpl = new Ext.XTemplate(
+ * 'Name: {name}
',
+ * 'Kids: ',
+ * '',
+ * '',
+ * '{name}
',
+ * ' ',
+ * '
'
+ * );
+ * tpl.overwrite(panel.body, data);
+ *
+ * More advanced conditionals are also supported:
+ *
+ * var tpl = new Ext.XTemplate(
+ * 'Name: {name}
',
+ * 'Kids: ',
+ * '',
+ * '{name} is a ',
+ * '',
+ * 'teenager
',
+ * '',
+ * 'kid
',
+ * '',
+ * 'baby
',
+ * ' ',
+ * '
'
+ * );
+ *
+ * var tpl = new Ext.XTemplate(
+ * 'Name: {name}
',
+ * 'Kids: ',
+ * '',
+ * '{name} is a ',
+ * '',
+ * '',
+ * 'girl
',
+ * '',
+ * 'boy
',
+ * ' ',
+ * '
'
+ * );
+ *
+ * A `break` is implied between each case and default, however, multiple cases can be listed
+ * in a single <tpl> tag.
+ *
+ * # Using double quotes
+ *
+ * Examples:
+ *
+ * var tpl = new Ext.XTemplate(
+ * "Child ",
+ * "Teenager ",
+ * "... ",
+ * '... ',
+ * " ",
+ * "Hello "
+ * );
+ *
+ * # Basic math support
+ *
+ * The following basic math operators may be applied directly on numeric data values:
+ *
+ * + - * /
+ *
+ * For example:
+ *
+ * var tpl = new Ext.XTemplate(
+ * 'Name: {name}
',
+ * 'Kids: ',
+ * '',
+ * '', // <-- Note that the > is encoded
+ * '{#}: {name}
', // <-- Auto-number each item
+ * 'In 5 Years: {age+5}
', // <-- Basic math
+ * 'Dad: {parent.name}
',
+ * ' ',
+ * '
'
+ * );
+ * tpl.overwrite(panel.body, data);
+ *
+ * # Execute arbitrary inline code with special built-in template variables
+ *
+ * Anything between `{[ ... ]}` is considered code to be executed in the scope of the template.
+ * The expression is evaluated and the result is included in the generated result. There are
+ * some special variables available in that code:
+ *
+ * - **out**: The output array into which the template is being appended (using `push` to later
+ * `join`).
+ * - **values**: The values in the current scope. If you are using scope changing sub-templates,
+ * you can change what values is.
+ * - **parent**: The scope (values) of the ancestor template.
+ * - **xindex**: If you are in a "for" or "foreach" looping template, the index of the loop you
+ * are in (1-based).
+ * - **xcount**: If you are in a "for" looping template, the total length of the array you are
+ * looping.
+ * - **xkey**: If you are in a "foreach" looping template, the key of the current property
+ * being examined.
+ *
+ * This example demonstrates basic row striping using an inline code block and the xindex variable:
+ *
+ * var tpl = new Ext.XTemplate(
+ * 'Name: {name}
',
+ * 'Company: {[values.company.toUpperCase() + ", " + values.title]}
',
+ * 'Kids: ',
+ * '',
+ * '',
+ * '{name}',
+ * '
',
+ * '
'
+ * );
+ *
+ * Any code contained in "verbatim" blocks (using "{% ... %}") will be inserted directly in
+ * the generated code for the template. These blocks are not included in the output. This
+ * can be used for simple things like break/continue in a loop, or control structures or
+ * method calls (when they don't produce output). The `this` references the template instance.
+ *
+ * var tpl = new Ext.XTemplate(
+ * 'Name: {name}
',
+ * 'Company: {[values.company.toUpperCase() + ", " + values.title]}
',
+ * 'Kids: ',
+ * '',
+ * '{% if (xindex % 2 === 0) continue; %}',
+ * '{name}',
+ * '{% if (xindex > 100) break; %}',
+ * '',
+ * '
'
+ * );
+ *
+ * # Template member functions
+ *
+ * One or more member functions can be specified in a configuration object passed into the
+ * XTemplate constructor for more complex processing:
+ *
+ * var tpl = new Ext.XTemplate(
+ * 'Name: {name}
',
+ * 'Kids: ',
+ * '',
+ * '',
+ * 'Girl: {name} - {age}
',
+ * '',
+ * 'Boy: {name} - {age}
',
+ * ' ',
+ * '',
+ * '{name} is a baby!
',
+ * ' ',
+ * '
',
+ * {
+ * // XTemplate configuration:
+ * disableFormats: true,
+ * // member functions:
+ * isGirl: function(name){
+ * return name == 'Aubrey' || name == 'Nikol';
+ * },
+ * isBaby: function(age){
+ * return age < 1;
+ * }
+ * }
+ * );
+ * tpl.overwrite(panel.body, data);
+ */
+Ext.define('Ext.XTemplate', {
+ extend: Ext.Template,
+ isXTemplate: true,
+ /**
+ * @private
+ */
+ emptyObj: {},
+ /**
+ * @cfg {Boolean} compiled
+ * Only applies to {@link Ext.Template}, XTemplates are compiled automatically on the
+ * first call to {@link #apply} or {@link #applyOut}.
+ * @hide
+ */
+ /**
+ * @cfg {String/Array} definitions
+ * Optional. A statement, or array of statements which set up `var`s which may then
+ * be accessed within the scope of the generated function.
+ *
+ * var data = {
+ * name: 'Don Griffin',
+ * isWizard: true,
+ * title: 'Senior Technomage',
+ * company: 'Sencha Inc.'
+ * };
+ *
+ * var tpl = new Ext.XTemplate('{[values.isWizard ? wizard : notSoWizard]}' +
+ * ' {name}', {
+ * definitions: 'var wizard = "Wizard", notSoWizard = "Townsperson";'
+ * });
+ *
+ * console.log(tpl.apply(data));
+ * // LOGS: Wizard Don Griffin
+ */
+ /**
+ * @property {Function} fn
+ * The function that applies this template. This is created on first use of the
+ * template (calls to `apply` or `applyOut`).
+ * @private
+ * @readonly
+ */
+ fn: null,
+ /**
+ * @cfg {Boolean} [strict=false]
+ * Expressions in templates that traverse "dot paths" and fail (due to `null` at some
+ * stage) have always been expanded as empty strings. This is convenient in most cases
+ * but doing so can also mask errors in the template. Setting this to `true` changes
+ * this default so that any expression errors will be thrown as exceptions.
+ */
+ strict: false,
+ apply: function(values, parent, xindex, xcount) {
+ var buffer = this.applyOut(values, [], parent, xindex, xcount);
+ // If only one token, return it as its uncoerced data type.
+ // This will allow things like ObjectTemplate to use
+ // formatters on non-string values.
+ return buffer.length === 1 ? buffer[0] : buffer.join('');
+ },
+ applyOut: function(values, out, parent, xindex, xcount) {
+ var me = this,
+ compiler;
+ if (!me.fn) {
+ compiler = new Ext.util.XTemplateCompiler({
+ useFormat: me.disableFormats !== true,
+ definitions: me.definitions,
+ strict: me.strict
+ });
+ me.fn = compiler.compile(me.html);
+ }
+ // xindex is 1-based, so 0 is impossible
+ xindex = xindex || 1;
+ // likewise, this tpl exists in the parent, so xcount==0 is not possible
+ xcount = xcount || 1;
+ if (me.strict) {
+ me.fn(out, values, parent || me.emptyObj, xindex, xcount);
+ } else {
+ try {
+ me.fn(out, values, parent || me.emptyObj, xindex, xcount);
+ } catch (e) {
+ Ext.log.warn('XTemplate evaluation exception: ' + e.message);
+ }
+ }
+ return out;
+ },
+ /**
+ * Does nothing. XTemplates are compiled automatically, so this function simply returns this.
+ * @return {Ext.XTemplate} this
+ */
+ compile: function() {
+ return this;
+ },
+ statics: {
+ get: function(config, source, defaultTpl) {
+ var ret = config;
+ if (config == null) {
+ if (source && defaultTpl) {
+ ret = this.getTpl(source, defaultTpl);
+ }
+ } else if ((config || config === '') && !config.isTemplate) {
+ ret = new this(config);
+ }
+ return ret;
+ },
+ /**
+ * Gets an `XTemplate` from an object (an instance of an {@link Ext#define}'d class).
+ * Many times, templates are configured high in the class hierarchy and are to be
+ * shared by all classes that derive from that base. To further complicate matters,
+ * these templates are seldom actual instances but are rather configurations. For
+ * example:
+ *
+ * Ext.define('MyApp.Class', {
+ * extraCls: 'extra-class',
+ *
+ * someTpl: [
+ * '
',
+ * {
+ * // Member fn - outputs the owing class's extra CSS class
+ * emitClass: function(out) {
+ * out.push(this.owner.extraCls);
+ * }
+ * }]
+ * });
+ *
+ * The goal being to share that template definition with all instances and even
+ * instances of derived classes, until `someTpl` is overridden. This method will
+ * "upgrade" these configurations to be real `XTemplate` instances *in place* (to
+ * avoid creating one instance per object).
+ *
+ * The resulting XTemplate will have an `owner` reference injected which refers back
+ * to the owning object whether that is an object which has an *own instance*, or a
+ * class prototype. Through this link, XTemplate member functions will be able to access
+ * prototype properties of its owning class.
+ *
+ * @param {Object} instance The object from which to get the `XTemplate` (must be
+ * an instance of an {@link Ext#define}'d class).
+ * @param {String} name The name of the property by which to get the `XTemplate`.
+ * @return {Ext.XTemplate} The `XTemplate` instance or null if not found.
+ * @protected
+ * @static
+ */
+ getTpl: function(instance, name) {
+ var tpl = instance[name],
+ // go for it! 99% of the time we will get it!
+ owner;
+ if (tpl) {
+ // tpl is just a configuration (not an instance)
+ if (!tpl.isTemplate) {
+ // create the template instance from the configuration:
+ tpl = Ext.XTemplate.get(tpl);
+ }
+ if (!tpl.owner) {
+ // and replace the reference with the new instance:
+ if (instance.hasOwnProperty(name)) {
+ // the tpl is on the instance
+ owner = instance;
+ } else {
+ // must be somewhere in the prototype chain
+ /* eslint-disable-next-line max-len, no-empty */
+ for (owner = instance.self.prototype; owner && !owner.hasOwnProperty(name); owner = owner.superclass) {}
+ }
+ owner[name] = tpl;
+ tpl.owner = owner;
+ }
+ }
+ // else !tpl (no such tpl) or the tpl is an instance already... either way, tpl
+ // is ready to return
+ return tpl || null;
+ }
+ }
+});
+
+/**
+ * @private
+ */
+Ext.define('Ext.util.translatable.Dom', {
+ extend: Ext.util.translatable.Abstract,
+ alias: 'translatable.dom',
+ // also configures Factoryable
+ config: {
+ element: null
+ },
+ applyElement: function(element) {
+ if (!element) {
+ return;
+ }
+ return Ext.get(element);
+ },
+ updateElement: function() {
+ this.refresh();
+ },
+ translateXY: function(x, y) {
+ var element = this.getElement();
+ if (element && !element.destroyed) {
+ this.callParent([
+ x,
+ y
+ ]);
+ }
+ }
+});
+
+/**
+ * @class Ext.util.translatable.CssPosition
+ * @private
+ */
+Ext.define('Ext.util.translatable.CssPosition', {
+ extend: Ext.util.translatable.Dom,
+ alias: 'translatable.cssposition',
+ // also configures Factoryable
+ doTranslate: function(x, y) {
+ var domStyle = this.getElement().dom.style;
+ if (typeof x === 'number') {
+ domStyle.left = x + 'px';
+ }
+ if (typeof y === 'number') {
+ domStyle.top = y + 'px';
+ }
+ },
+ syncPosition: function() {
+ var domStyle = this.getElement().dom.style;
+ return [
+ this.x = parseFloat(domStyle.left),
+ this.y = parseFloat(domStyle.top)
+ ];
+ },
+ destroy: function() {
+ var domStyle = this.getElement().dom.style;
+ domStyle.left = null;
+ domStyle.top = null;
+ this.callParent();
+ }
+});
+
+/**
+ * @private
+ *
+ * CSS Transform implementation
+ */
+Ext.define('Ext.util.translatable.CssTransform', {
+ extend: Ext.util.translatable.Dom,
+ alias: 'translatable.csstransform',
+ // also configures Factoryable
+ isCssTransform: true,
+ posRegex: /(\d+)px[^\d]*(\d+)px/,
+ doTranslate: function(x, y) {
+ this.getElement().translate(x, y);
+ },
+ syncPosition: function() {
+ var pos = this.posRegex.exec(this.getElement().dom.style.tranform);
+ if (pos) {
+ this.x = parseFloat(pos[1]);
+ this.y = parseFloat(pos[2]);
+ }
+ return [
+ this.x,
+ this.y
+ ];
+ },
+ destroy: function() {
+ var element = this.getElement();
+ if (element && !element.destroyed) {
+ element.dom.style.webkitTransform = null;
+ }
+ this.callParent();
+ }
+});
+
+/**
+ * Ext.scroll.Scroller allows any element to have scrollable content, both on desktop and
+ * touch-screen devices, and defines a set of useful methods for manipulating the scroll
+ * position and controlling the scrolling behavior.
+ */
+Ext.define('Ext.scroll.Scroller', {
+ extend: Ext.Evented,
+ alias: 'scroller.scroller',
+ mixins: [
+ Ext.mixin.Factoryable,
+ Ext.mixin.Bufferable
+ ],
+ isScroller: true,
+ factoryConfig: {
+ defaultType: 'native'
+ },
+ bufferableMethods: {
+ onScrollEnd: 100
+ },
+ /**
+ * @event refresh
+ * Fires whenever the Scroller is refreshed.
+ * @param {Ext.scroll.Scroller} this
+ */
+ /**
+ * @event scrollstart
+ * Fires whenever the scrolling is started.
+ * @param {Ext.scroll.Scroller} this
+ * @param {Number} x The current x position
+ * @param {Number} y The current y position
+ */
+ /**
+ * @event scrollend
+ * Fires whenever the scrolling is ended.
+ * @param {Ext.scroll.Scroller} this
+ * @param {Number} x The current x position
+ * @param {Number} y The current y position
+ * @param {Number} deltaX The change in x value
+ * @param {Number} deltaY The change in y value
+ */
+ /**
+ * @event scroll
+ * Fires whenever the Scroller is scrolled.
+ * @param {Ext.scroll.Scroller} this
+ * @param {Number} x The new x position
+ * @param {Number} y The new y position
+ * @param {Number} deltaX The change in x value since the last scrollstart event
+ * @param {Number} deltaY The change in y value since the last scrollstart event
+ */
+ config: {
+ /**
+ * @cfg {'auto'/'vertical'/'horizontal'/'both'} [direction='auto']
+ * @deprecated 5.1.0 use {@link #x} and {@link #y} instead
+ */
+ direction: undefined,
+ // undefined because we need the updater to always run
+ /**
+ * @cfg {String/HTMLElement/Ext.dom.Element}
+ * The element to make scrollable.
+ */
+ element: null,
+ /**
+ * @cfg {Object}
+ * The size of the scrollable content expressed as an object with x and y properties
+ * @private
+ * @readonly
+ */
+ size: null,
+ /**
+ * @cfg {Boolean/String}
+ * - `true` or `'auto'` to enable horizontal auto-scrolling. In auto-scrolling mode
+ * scrolling is only enabled when the {@link #element} has overflowing content.
+ * - `false` to disable horizontal scrolling
+ * - `'scroll'` to always enable horizontal scrolling regardless of content size.
+ */
+ x: true,
+ /**
+ * @cfg {Boolean/String}
+ * - `true` or `'auto'` to enable vertical auto-scrolling. In auto-scrolling mode
+ * scrolling is only enabled when the {@link #element} has overflowing content.
+ * - `false` to disable vertical scrolling
+ * - `'scroll'` to always enable vertical scrolling regardless of content size.
+ */
+ y: true,
+ /**
+ * @cfg {Object} touchAction for the scroller's {@link #element}.
+ *
+ * For more details see {@link Ext.dom.Element#setTouchAction}
+ */
+ touchAction: null,
+ /**
+ * @private
+ */
+ translatable: 'scrollposition'
+ },
+ constructor: function(config) {
+ var me = this;
+ me.position = {
+ x: 0,
+ y: 0
+ };
+ me.configuredSize = {
+ x: false,
+ y: false
+ };
+ me.callParent([
+ config
+ ]);
+ },
+ destroy: function() {
+ var me = this;
+ me.destroying = true;
+ me.destroy = Ext.emptyFn;
+ me.doDestroy();
+ me.callParent();
+ },
+ // This just makes it hard to ask "was destroy() called?":
+ // me.destroying = false; // removed in 7.0
+ doDestroy: function() {
+ var me = this,
+ partners, id;
+ me.component = null;
+ partners = me._partners;
+ if (partners) {
+ for (id in partners) {
+ me.removePartner(partners[id].scroller);
+ }
+ }
+ me._partners = null;
+ me.setElement(null);
+ me.setTranslatable(null);
+ },
+ /**
+ * Adds a "partner" scroller. Partner scrollers reflect each other's scroll position
+ * at all times - if either scroller is scrolled, the scroll position of its partner
+ * will be be automatically synchronized.
+ *
+ * A scroller may have multiple partners.
+ *
+ * @param {Ext.scroll.Scroller} partner
+ * @param {String} [axis='both'] The axis to synchronize (`'x'`, '`y`', or '`both`')
+ */
+ addPartner: function(partner, axis) {
+ var me = this,
+ partners = me._partners || (me._partners = {}),
+ otherPartners = partner._partners || (partner._partners = {});
+ // Translate to boolean flags. {x:,y:}
+ axis = me.axisConfigs[axis || 'both'];
+ partners[partner.getId()] = {
+ scroller: partner,
+ axes: axis,
+ called: false,
+ calledPrimary: false
+ };
+ otherPartners[me.getId()] = {
+ scroller: me,
+ axes: axis,
+ called: false,
+ calledPrimary: false
+ };
+ },
+ // hook for rtl mode to convert an x coordinate to RTL space.
+ convertX: function(x) {
+ return x;
+ },
+ /**
+ * Ensures a descendant element of the scroller is visible by scrolling to it.
+ *
+ * @param {Object/String/HTMLElement/Ext.dom.Element} el
+ * The descendant element to scroll into view. May also be the options object with
+ * the `element` key defining the descendant element.
+ *
+ * @param {Object} [options] An object containing options to modify the operation.
+ *
+ * @param {Object} [options.align] The alignment for the scroll.
+ * @param {'start'/'center'/'end'} [options.align.x] The alignment of the x scroll. If not
+ * specified, the minimum will be done to make the element visible. The behavior is `undefined`
+ * if the request cannot be honored. If the alignment is suffixed with a `?`, the alignment will
+ * only take place if the item is not already in the visible area.
+ * @param {'start'/'center'/'end'} [options.align.y] The alignment of the y scroll. If not
+ * specified, the minimum will be done to make the element visible. The behavior is `undefined`
+ * if the request cannot be honored. If the alignment is suffixed with a `?`, the alignment will
+ * only take place if the item is not already in the visible area.
+ *
+ * @param {Boolean} [options.animation] Pass `true` to animate the row into view.
+ *
+ * @param {Boolean} [options.highlight=false] Pass `true` to highlight the row with a glow
+ * animation when it is in view. Can also be a hex color to use for highlighting
+ * (defaults to yellow = '#ffff9c').
+ *
+ * @param {Boolean} [options.x=true] `false` to disable horizontal scroll.
+ * @param {Boolean} [options.y=true] `false` to disable vertical scroll.
+ *
+ * @return {Ext.Promise} A promise for when the scroll completes.
+ * @since 6.5.1
+ */
+ ensureVisible: function(el, options) {
+ var me = this,
+ position = me.getPosition(),
+ highlight, newPosition, ret;
+ // Might get called before Component#onBoxReady which is when the Scroller is set up with
+ // elements.
+ if (el) {
+ if (el && el.element && !el.isElement) {
+ options = el;
+ el = options.element;
+ }
+ options = options || {};
+ highlight = options.highlight;
+ newPosition = me.getEnsureVisibleXY(el, options);
+ // Only attempt to scroll if it's needed.
+ if (newPosition.y !== position.y || newPosition.x !== position.x) {
+ if (highlight) {
+ me.on({
+ scrollend: 'doHighlight',
+ scope: me,
+ single: true,
+ args: [
+ el,
+ highlight
+ ]
+ });
+ }
+ ret = me.doScrollTo(newPosition.x, newPosition.y, options.animation);
+ } else {
+ // No scrolling needed, but still honor highlight request
+ if (highlight) {
+ me.doHighlight(el, highlight);
+ }
+ // Resolve straight away
+ ret = Ext.Deferred.getCachedResolved();
+ }
+ } else {
+ // Can't scroll
+ ret = Ext.Deferred.getCachedRejected();
+ }
+ return ret;
+ },
+ /**
+ * @method getClientSize
+ * Gets the `clientWidth` and `clientHeight` of the {@link #element} for this scroller.
+ * @return {Object} An object with `x` and `y` properties.
+ */
+ /**
+ * @method getMaxPosition
+ * Returns the maximum scroll position for this scroller
+ * @return {Object} position
+ * @return {Number} return.x The maximum scroll position on the x axis
+ * @return {Number} return.y The maximum scroll position on the y axis
+ */
+ /**
+ * @method getMaxUserPosition
+ * Returns the maximum scroll position for this scroller for scrolling that is initiated
+ * by the user via mouse or touch. This differs from getMaxPosition in that getMaxPosition
+ * returns the true maximum scroll position regardless of which axes are enabled for
+ * user scrolling.
+ * @return {Object} position
+ * @return {Number} return.x The maximum scroll position on the x axis
+ * @return {Number} return.y The maximum scroll position on the y axis
+ */
+ /**
+ * @method getPosition
+ * Returns the current scroll position
+ * @return {Object} An object with `x` and `y` properties.
+ */
+ getPosition: function() {
+ return this.position;
+ },
+ /**
+ * @method getScrollbarSize
+ * Returns the amount of space consumed by scrollbars in the DOM
+ * @return {Object} size An object containing the scrollbar sizes.
+ * @return {Number} return.width The width of the vertical scrollbar.
+ * @return {Number} return.height The height of the horizontal scrollbar.
+ */
+ /**
+ * Determines if the passed element is within the visible x and y scroll viewport.
+ * @param {String/HTMLElement/Ext.dom.Element} el The dom node, Ext.dom.Element, or
+ * id (string) of the dom element that is to be verified to be in view
+ * @param {Boolean} [contains=true] `false` to skip checking if the scroller contains
+ * the passed element in the dom. When `false` the element is considered to be
+ * "in view" if its location on the page is within the scroller's client region.
+ * Passing `false` improves performance when the element is already known to be
+ * contained by this scroller.
+ * @return {Object} Which ranges the element is in.
+ * @return {Boolean} return.x `true` if the passed element is within the x visible range.
+ * @return {Boolean} return.y `true` if the passed element is within the y visible range.
+ */
+ isInView: function(el, contains) {
+ var me = this,
+ c = me.component,
+ result = {
+ x: false,
+ y: false
+ },
+ myEl = me.getElement(),
+ elRegion, myElRegion;
+ if (el && (contains === false || myEl.contains(el) || (c && c.owns(el)))) {
+ myElRegion = myEl.getRegion();
+ elRegion = Ext.fly(el).getRegion();
+ result.x = elRegion.right > myElRegion.left && elRegion.left < myElRegion.right;
+ result.y = elRegion.bottom > myElRegion.top && elRegion.top < myElRegion.bottom;
+ }
+ return result;
+ },
+ /**
+ * Refreshes the scroller size and maxPosition.
+ * @param {Boolean} immediate `true` to refresh immediately. By default refreshes
+ * are deferred until the next {@link Ext.GlobalEvents#event-idle idle} event to
+ * ensure any pending writes have been flushed to the dom and any reflows have
+ * taken place.
+ * @return {Ext.scroll.Scroller} this
+ * @chainable
+ */
+ refresh: function() {
+ this.fireEvent('refresh', this);
+ return this;
+ },
+ /**
+ * Removes a partnership that was created via {@link #addPartner}
+ * @param {Ext.scroll.Scroller} partner
+ * @private
+ */
+ removePartner: function(partner) {
+ var partners = this._partners,
+ otherPartners = partner._partners;
+ if (partners) {
+ delete partners[partner.getId()];
+ }
+ if (otherPartners) {
+ delete (otherPartners[this.getId()]);
+ }
+ },
+ /**
+ * Scrolls by the passed delta values, optionally animating.
+ *
+ * All of the following are equivalent:
+ *
+ * scroller.scrollBy(10, 10, true);
+ * scroller.scrollBy([10, 10], true);
+ * scroller.scrollBy({ x: 10, y: 10 }, true);
+ *
+ * A null value for either `x` or `y` will result in no scrolling on the given axis,
+ * for example:
+ *
+ * scroller.scrollBy(null, 10);
+ *
+ * will scroll by 10 on the y axis and leave the x axis at its current scroll position
+ *
+ * @param {Number/Number[]/Object} deltaX Either the x delta, an Array specifying x
+ * and y deltas or an object with "x" and "y" properties.
+ * @param {Number/Boolean/Object} deltaY Either the y delta, or an animate flag or
+ * config object.
+ * @param {Boolean/Object} animate Animate flag/config object if the delta values were
+ * passed separately.
+ * @return {Ext.Promise} A promise for when the scroll completes.
+ */
+ scrollBy: function(deltaX, deltaY, animate) {
+ var position = this.getPosition();
+ if (deltaX) {
+ if (deltaX.length) {
+ // array
+ animate = deltaY;
+ deltaY = deltaX[1];
+ deltaX = deltaX[0];
+ } else if (typeof deltaX !== 'number') {
+ // object
+ animate = deltaY;
+ deltaY = deltaX.y;
+ deltaX = deltaX.x;
+ }
+ }
+ deltaX = (typeof deltaX === 'number') ? deltaX + position.x : null;
+ deltaY = (typeof deltaY === 'number') ? deltaY + position.y : null;
+ return this.doScrollTo(deltaX, deltaY, animate);
+ },
+ /**
+ * Scrolls a descendant element of the scroller into view.
+ * @param {String/HTMLElement/Ext.dom.Element} el the descendant to scroll into view
+ * @param {Boolean} [hscroll=true] False to disable horizontal scroll.
+ * @param {Boolean/Object} [animate] true for the default animation or a standard Element
+ * animation config object
+ * @param {Boolean/String} [highlight=false] true to
+ * {@link Ext.dom.Element#highlight} the element when it is in view. Can also be a
+ * hex color to use for highlighting (defaults to yellow = '#ffff9c').
+ *
+ * @deprecated 6.5.1 Use {@link #ensureVisible} instead.
+ * @return {Ext.Promise} A promise for when the scroll completes.
+ */
+ scrollIntoView: function(el, hscroll, animate, highlight) {
+ return this.ensureVisible(el, {
+ animation: animate,
+ highlight: highlight,
+ x: hscroll
+ });
+ },
+ /**
+ * Scrolls to the given position.
+ *
+ * All of the following are equivalent:
+ *
+ * scroller.scrollTo(10, 10, true);
+ * scroller.scrollTo([10, 10], true);
+ * scroller.scrollTo({ x: 10, y: 10 }, true);
+ *
+ * A null value for either `x` or `y` will result in no scrolling on the given axis,
+ * for example:
+ *
+ * scroller.scrollTo(null, 10);
+ *
+ * will scroll to 10 on the y axis and leave the x axis at its current scroll position
+ *
+ * A negative value for either `x` or `y` represents an offset from the maximum scroll
+ * position on the given axis:
+ *
+ * // scrolls to 10px from the maximum x scroll position and 20px from maximum y
+ * scroller.scrollTo(-10, -20);
+ *
+ * A value of Infinity on either axis will scroll to the maximum scroll position on
+ * that axis:
+ *
+ * // scrolls to the maximum position on both axes
+ * scroller.scrollTo(Infinity, Infinity);
+ *
+ * @param {Number} x The scroll position on the x axis.
+ * @param {Number} y The scroll position on the y axis.
+ * @param {Boolean/Object} [animation] Whether or not to animate the scrolling to the new
+ * position.
+ *
+ * @return {Ext.Promise} A promise for when the scroll completes.
+ */
+ scrollTo: function(x, y, animation) {
+ var me = this,
+ maxPosition;
+ if (x) {
+ if (Ext.isArray(x)) {
+ animation = y;
+ y = x[1];
+ x = x[0];
+ } else if (typeof x !== 'number') {
+ animation = y;
+ y = x.y;
+ x = x.x;
+ }
+ }
+ if (x < 0 || y < 0) {
+ maxPosition = me.getMaxPosition();
+ if (x < 0) {
+ x += maxPosition.x;
+ }
+ if (y < 0) {
+ y += maxPosition.y;
+ }
+ }
+ return me.doScrollTo(x, y, animation);
+ },
+ //-------------------------
+ // Public Configs
+ // element
+ applyElement: function(element) {
+ if (element) {
+ if (typeof element === 'string' && !Ext.get(element)) {
+ Ext.raise("Cannot create Ext.scroll.Scroller instance. " + "Element with id '" + element + "' not found.");
+ }
+ if (!element.isElement) {
+ element = Ext.get(element);
+ }
+ }
+ return element;
+ },
+ updateElement: function(element, oldElement) {
+ var me = this,
+ scrollerCls = me.scrollerCls,
+ touchAction;
+ if (oldElement && !oldElement.destroyed) {
+ oldElement.removeCls(scrollerCls);
+ }
+ if (element) {
+ if (!this.isConfiguring) {
+ touchAction = this.getTouchAction();
+ if (touchAction) {
+ element.setTouchAction(touchAction);
+ }
+ }
+ element.addCls(scrollerCls);
+ }
+ },
+ // direction
+ updateDirection: function(direction) {
+ var me = this,
+ x, y;
+ if (!direction) {
+ // if no direction was configured we set its value based on the values of
+ // x and y. This ensures getDirection() always returns something useful
+ // for backward compatibility.
+ x = me.getX();
+ y = me.getY();
+ if (x && y) {
+ direction = (y === 'scroll' && x === 'scroll') ? 'both' : 'auto';
+ } else if (y) {
+ direction = 'vertical';
+ } else if (x) {
+ direction = 'horizontal';
+ }
+ // set the _direction property directly to avoid the updater being called
+ // and triggering setX/setY calls
+ me._direction = direction;
+ } else {
+ if (direction === 'auto') {
+ x = true;
+ y = true;
+ } else if (direction === 'vertical') {
+ x = false;
+ y = true;
+ } else if (direction === 'horizontal') {
+ x = true;
+ y = false;
+ } else if (direction === 'both') {
+ x = 'scroll';
+ y = 'scroll';
+ }
+ me.setX(x);
+ me.setY(y);
+ }
+ },
+ // size
+ applySize: function(size, oldSize) {
+ var num = 'number',
+ configuredSize = this.configuredSize,
+ x, y;
+ if (size == null || typeof size === num) {
+ x = y = size;
+ } else if (size) {
+ x = size.x;
+ y = size.y;
+ }
+ if (typeof x === num) {
+ configuredSize.x = true;
+ } else if (x === null) {
+ configuredSize.x = false;
+ }
+ if (typeof y === num) {
+ configuredSize.y = true;
+ } else if (y === null) {
+ configuredSize.y = false;
+ }
+ if (x === undefined) {
+ x = (oldSize ? oldSize.x : null);
+ }
+ if (y === undefined) {
+ y = (oldSize ? oldSize.y : null);
+ }
+ return (oldSize && x === oldSize.x && y === oldSize.y) ? oldSize : {
+ x: x,
+ y: y
+ };
+ },
+ // touchAction
+ updateTouchAction: function(touchAction) {
+ var element = this.getElement();
+ if (element) {
+ element.setTouchAction(touchAction);
+ }
+ },
+ //-----------------------------------------------------------------------
+ statics: {
+ /**
+ * Creates and returns an appropriate Scroller instance for the current device.
+ * @param {Object} config Configuration options for the Scroller
+ * @param type
+ * @return {Ext.scroll.Scroller}
+ */
+ create: function(config, type) {
+ return Ext.Factory.scroller(config, type);
+ },
+ /**
+ * @private
+ */
+ getScrollingElement: function() {
+ return Ext.get(document.scrollingElement || document.documentElement);
+ }
+ },
+ privates: {
+ scrollerCls: Ext.baseCSSPrefix + 'scroller',
+ axisConfigs: {
+ x: {
+ x: 1,
+ y: 0
+ },
+ y: {
+ x: 0,
+ y: 1
+ },
+ both: {
+ x: 1,
+ y: 1
+ }
+ },
+ callPartners: function(method, scrollX, scrollY) {
+ var me = this,
+ partners = me._partners,
+ axes, id, partner, pos, scroller, x, y;
+ if (!me.suspendSync) {
+ for (id in partners) {
+ partner = partners[id];
+ scroller = partner.scroller;
+ if (!scroller.isPrimary && !partner.called) {
+ partner.called = true;
+ // this flag avoids infinite recursion
+ axes = partners[id].axes;
+ pos = scroller.position;
+ x = (!axes.x || scrollX === undefined) ? pos.x : scrollX;
+ y = (!axes.y || scrollY === undefined) ? pos.y : scrollY;
+ scroller[method](x, y, (x - pos.x) || 0, (y - pos.y) || 0);
+ scroller.callPartners(method, x, y);
+ partner.called = false;
+ }
+ }
+ }
+ },
+ /**
+ * Checks if the scroller contains a component by searching up the element hierarchy
+ * using components. It uses component navigation as opposed to elements because we
+ * want logical ownership.
+ * @private
+ */
+ contains: function(component) {
+ var el = this.getElement(),
+ owner = component;
+ while (owner && owner !== Ext.Viewport) {
+ if (el.contains(owner.el)) {
+ return true;
+ }
+ owner = owner.getRefOwner();
+ }
+ return false;
+ },
+ fireScrollStart: function(x, y, deltaX, deltaY) {
+ var me = this,
+ component = me.component;
+ me.startX = x - deltaX;
+ me.startY = y - deltaY;
+ if (me.hasListeners.scrollstart) {
+ me.fireEvent('scrollstart', me, x, y);
+ }
+ if (component && component.onScrollStart) {
+ component.onScrollStart(x, y);
+ }
+ Ext.GlobalEvents.fireEvent('scrollstart', me, x, y);
+ },
+ fireScroll: function(x, y, deltaX, deltaY) {
+ var me = this,
+ component = me.component;
+ if (me.hasListeners.scroll) {
+ me.fireEvent('scroll', me, x, y, deltaX, deltaY);
+ }
+ if (component && component.onScrollMove) {
+ component.onScrollMove(x, y, deltaX, deltaY);
+ }
+ Ext.fireEvent('scroll', me, x, y, deltaX, deltaY);
+ },
+ fireScrollEnd: function(x, y) {
+ var me = this,
+ component = me.component,
+ deltaX = x - me.startX,
+ deltaY = y - me.startY;
+ if (me.hasListeners.scrollend) {
+ me.fireEvent('scrollend', me, x, y, deltaX, deltaY);
+ }
+ if (component && component.onScrollEnd) {
+ component.onScrollEnd(x, y, deltaX, deltaY);
+ }
+ Ext.fireEvent('scrollend', me, x, y, deltaX, deltaY);
+ },
+ /**
+ * @private
+ * Gets the x/y coordinates to ensure the element is scrolled into view.
+ *
+ * @param {String/HTMLElement/Ext.dom.Element/Object} el
+ * The descendant element to scroll into view. May also be the options object with
+ * the `element` key defining the descendant element.
+ *
+ * @param {Object} [options] An object containing options to modify the operation.
+ *
+ * @param {Object/String} [options.align] The alignment for the scroll. If a string,
+ * this value will be used for both `x` and `y` alignments.
+ * @param {'start'/'center'/'end'} [options.align.x] The alignment of the x scroll. If not
+ * specified, the minimum will be done to make the element visible. The behavior is
+ * `undefined` if the request cannot be honored. If the alignment is suffixed with a `?`,
+ * the alignment will only take place if the item is not already in the visible area.
+ * @param {'start'/'center'/'end'} [options.align.y] The alignment of the y scroll. If not
+ * specified, the minimum will be done to make the element visible. The behavior is
+ * `undefined` if the request cannot be honored. If the alignment is suffixed with a `?`,
+ * the alignment will only take place if the item is not already in the visible area.
+ *
+ * @param {Boolean} [options.x=true] `false` to disable horizontal scroll and `x` align
+ * option.
+ * @param {Boolean} [options.y=true] `false` to disable vertical scroll and `y` align
+ * option.
+ * @return {Object} The new position that will be used to scroll the element into view.
+ * @since 6.5.1
+ */
+ getEnsureVisibleXY: function(el, options) {
+ var position = this.getPosition(),
+ viewport = this.component ? this.component.getScrollableClientRegion() : this.getElement(),
+ newPosition, align;
+ if (el && el.element && !el.isElement) {
+ options = el;
+ el = options.element;
+ }
+ options = options || {};
+ align = options.align;
+ if (align) {
+ if (Ext.isString(align)) {
+ align = {
+ x: options.x === false ? null : align,
+ y: options.y === false ? null : align
+ };
+ } else if (Ext.isObject(align)) {
+ if (align.x && options.x === false) {
+ align.x = null;
+ }
+ if (align.y && options.y === false) {
+ align.y = null;
+ }
+ }
+ }
+ newPosition = Ext.fly(el).getScrollIntoViewXY(viewport, position.x, position.y, align);
+ newPosition.x = options.x === false ? position.x : newPosition.x;
+ newPosition.y = options.y === false ? position.y : newPosition.y;
+ return newPosition;
+ },
+ onPartnerScroll: function(x, y) {
+ this.doScrollTo(x, y);
+ },
+ onPartnerScrollStart: function(x, y, deltaX, deltaY) {
+ this.isScrolling = true;
+ if (deltaX || deltaY) {
+ this.doScrollTo(x, y);
+ }
+ },
+ onPartnerScrollEnd: function() {
+ this.isScrolling = false;
+ },
+ /**
+ * In IE8, IE9, and IE10 when using native scrolling the scroll position is reset
+ * to 0,0 when the scrolling element is hidden. This method may be called to restore
+ * the scroll after hiding and showing the element.
+ */
+ restoreState: Ext.privateFn,
+ resumePartnerSync: function(syncNow) {
+ var me = this,
+ position, x, y;
+ if (me.suspendSync) {
+ me.suspendSync--;
+ }
+ if (!me.suspendSync && syncNow) {
+ position = me.position;
+ x = position.x;
+ y = position.y;
+ me.callPartners('onPartnerScrollStart', undefined, undefined);
+ me.callPartners('fireScrollStart', undefined, undefined);
+ me.callPartners('onPartnerScroll', x, y);
+ me.callPartners('fireScroll', x, y);
+ me.callPartners('onPartnerScrollEnd');
+ me.callPartners('fireScrollEnd', x, y);
+ }
+ },
+ /**
+ * Sets this scroller as the "primary" scroller in a partnership. When true
+ * sets a `isPrimary` property to true on the primary scroller and recursively sets
+ * the same property to `false` on the partners
+ * @param {Boolean} isPrimary
+ * @private
+ */
+ setPrimary: function(isPrimary) {
+ var me = this,
+ partners = me._partners,
+ partner, scroller, id;
+ if (isPrimary) {
+ for (id in partners) {
+ partner = partners[id];
+ scroller = partner.scroller;
+ if (!partner.calledPrimary) {
+ partner.calledPrimary = true;
+ // this flag avoids infinite recursion
+ scroller.setPrimary(false);
+ partner.calledPrimary = false;
+ }
+ }
+ me.isScrolling = Ext.isScrolling = true;
+ } else if (me.isScrolling) {
+ me.cancelOnScrollEnd();
+ me.doOnScrollEnd();
+ }
+ me.isPrimary = isPrimary;
+ },
+ suspendPartnerSync: function() {
+ this.suspendSync = (this.suspendSync || 0) + 1;
+ },
+ /**
+ * @private
+ * May be called when a Component is rendered AFTER some scrolling partner has begun its
+ * lifecycle to sync this scroller with partners which may be scrolled anywhere by now.
+ */
+ syncWithPartners: function() {
+ var me = this,
+ partners = me._partners,
+ position = me.position,
+ id, axes, xAxis, yAxis, partner, partnerPosition, x, y, deltaX, deltaY;
+ for (id in partners) {
+ axes = partners[id].axes;
+ xAxis = axes.x;
+ yAxis = axes.y;
+ partner = partners[id].scroller;
+ partnerPosition = partner.position;
+ partner.setPrimary(true);
+ x = xAxis ? position.x : null;
+ y = yAxis ? position.y : null;
+ deltaX = deltaY = 0;
+ me.onPartnerScrollStart(x, y, 0, 0);
+ me.fireScrollStart(x, y, 0, 0);
+ x = xAxis ? partnerPosition.x : null;
+ y = yAxis ? partnerPosition.y : null;
+ deltaX = (x === null) ? 0 : (x - position.x);
+ deltaY = (y === null) ? 0 : (y - position.y);
+ me.onPartnerScroll(x, y);
+ me.fireScroll(x, y, deltaX, deltaY);
+ me.onPartnerScrollEnd();
+ me.fireScrollEnd(x, y);
+ partner.setPrimary(null);
+ }
+ },
+ //-------------------------
+ // Private Configs
+ // translatable
+ applyTranslatable: function(translatable, oldTranslatable) {
+ return Ext.Factory.translatable.update(oldTranslatable, translatable, this, 'createTranslatable');
+ }
+ },
+ deprecated: {
+ '5': {
+ methods: {
+ /**
+ * @method getScroller
+ * Returns this scroller.
+ *
+ * In Sencha Touch 2, access to a Component's Scroller was provided via
+ * a Ext.scroll.View class that was returned from the Component's getScrollable()
+ * method:
+ *
+ * component.getScrollable().getScroller();
+ *
+ * in 5.0 all the functionality of Ext.scroll.View has been rolled into
+ * Ext.scroll.Scroller, and Ext.scroll.View has been removed. Component's
+ * getScrollable() method now returns a Ext.scroll.Scroller. This method is
+ * provided for compatibility.
+ * @deprecated 5.0 This method is deprecated. Please use Ext.scroll.Scroller's
+ * getScrollable() method instead.
+ */
+ getScroller: function() {
+ return this;
+ }
+ }
+ },
+ '5.1.0': {
+ methods: {
+ /**
+ * @method scrollToTop
+ * Scrolls to 0 on both axes
+ * @param {Boolean/Object} animate
+ * @private
+ * @return {Ext.scroll.Scroller} this
+ * @chainable
+ * @deprecated 5.1.0 Use scrollTo instead
+ */
+ scrollToTop: function(animate) {
+ return this.scrollTo(0, 0, animate);
+ },
+ /**
+ * @method scrollToEnd
+ * Scrolls to the maximum position on both axes
+ * @param {Boolean/Object} animate
+ * @private
+ * @return {Ext.scroll.Scroller} this
+ * @chainable
+ * @deprecated 5.1.0 Use scrollTo instead
+ */
+ scrollToEnd: function(animate) {
+ return this.scrollTo(Infinity, Infinity, animate);
+ }
+ }
+ }
+ }
+});
+
+/**
+ * Most of the visual classes you interact with are Components. Every Component is a
+ * subclass of Ext.Component, which means they can all:
+ *
+ * * Render themselves onto the page using a template
+ * * Show and hide themselves at any time
+ * * Center themselves within their parent container
+ * * Enable and disable themselves
+ *
+ * They can also do a few more advanced things:
+ *
+ * * Float above other components (windows, message boxes and overlays)
+ * * Change size and position on the screen with animation
+ * * Dock other Components inside themselves (useful for toolbars)
+ * * Align to other components
+ * * Allow themselves to be dragged around
+ * * Make their content scrollable & more
+ *
+ * ## Available Components
+ *
+ * There are many components. They are separated into 4 main groups:
+ *
+ * ### Navigation components
+ * * {@link Ext.Toolbar}
+ * * {@link Ext.Button}
+ * * {@link Ext.TitleBar}
+ * * {@link Ext.SegmentedButton}
+ * * {@link Ext.Title}
+ * * {@link Ext.Spacer}
+ *
+ * ### Store-bound components
+ * * {@link Ext.dataview.DataView}
+ * * {@link Ext.Carousel}
+ * * {@link Ext.List}
+ * * {@link Ext.NestedList}
+ *
+ * ### Form components
+ * * {@link Ext.form.Panel}
+ * * {@link Ext.form.FieldSet}
+ * * {@link Ext.field.Checkbox}
+ * * {@link Ext.field.Hidden}
+ * * {@link Ext.field.Slider}
+ * * {@link Ext.field.Text}
+ * * {@link Ext.picker.Picker}
+ * * {@link Ext.picker.Date}
+ *
+ * ### General components
+ * * {@link Ext.Panel}
+ * * {@link Ext.tab.Panel}
+ * * {@link Ext.Viewport Ext.Viewport}
+ * * {@link Ext.Img}
+ * * {@link Ext.Audio}
+ * * {@link Ext.Video}
+ * * {@link Ext.Sheet}
+ * * {@link Ext.ActionSheet}
+ * * {@link Ext.MessageBox}
+ *
+ *
+ * ## Instantiating Components
+ *
+ * Components are created the same way as all other classes - using Ext.create. Here's how we can
+ * create a Text field:
+ *
+ * var panel = Ext.create('Ext.Panel', {
+ * html: 'This is my panel'
+ * });
+ *
+ * This will create a {@link Ext.Panel Panel} instance, configured with some basic HTML content. A
+ * Panel is just a simple Component that can render HTML and also contain other items. In this case
+ * we've created a Panel instance but it won't show up on the screen yet because items are not
+ * rendered immediately after being instantiated. This allows us to create some components and move
+ * them around before rendering and laying them out, which is a good deal faster than moving them
+ * after rendering.
+ *
+ * To show this panel on the screen now we can simply add it to the global Viewport:
+ *
+ * Ext.Viewport.add(panel);
+ *
+ * Panels are also Containers, which means they can contain other Components, arranged by a layout.
+ * Let's revisit the above example now, this time creating a panel with two child Components and a
+ * hbox layout:
+ *
+ * @example
+ * var panel = Ext.create('Ext.Panel', {
+ * layout: 'hbox',
+ *
+ * items: [
+ * {
+ * xtype: 'panel',
+ * flex: 1,
+ * html: 'Left Panel, 1/3rd of total size',
+ * style: 'background-color: #5E99CC;'
+ * },
+ * {
+ * xtype: 'panel',
+ * flex: 2,
+ * html: 'Right Panel, 2/3rds of total size',
+ * style: 'background-color: #759E60;'
+ * }
+ * ]
+ * });
+ *
+ * Ext.Viewport.add(panel);
+ *
+ * This time we created 3 Panels - the first one is created just as before but the inner two are
+ * declared inline using an xtype. Xtype is a convenient way of creating Components without having
+ * to go through the process of using Ext.create and specifying the full class name, instead you
+ * can just provide the xtype for the class inside an object and the framework will create the
+ * components for you.
+ *
+ * We also specified a layout for the top level panel - in this case hbox, which splits the
+ * horizontal width of the parent panel based on the 'flex' of each child. For example, if the
+ * parent Panel above is 300px wide then the first child will be flexed to 100px wide and the
+ * second to 200px because the first one was given `flex: 1` and the second `flex: 2`.
+ *
+ * ## Using xtype
+ *
+ * xtype is an easy way to create Components without using the full class name. This is especially
+ * useful when creating a {@link Ext.Container Container} that contains child Components. An xtype
+ * is simply a shorthand way of specifying a Component - for example you can use `xtype: 'panel'`
+ * instead of typing out Ext.Panel.
+ *
+ * Sample usage:
+ *
+ * @example
+ * Ext.create('Ext.Container', {
+ * fullscreen: true,
+ * layout: 'fit',
+ *
+ * items: [
+ * {
+ * xtype: 'panel',
+ * html: 'This panel is created by xtype'
+ * },
+ * {
+ * xtype: 'toolbar',
+ * title: 'So is the toolbar',
+ * docked: 'top'
+ * }
+ * ]
+ * });
+ *
+ *
+ * ### Common xtypes
+ *
+ *
+ xtype Class
+ ----------------- ---------------------
+ actionsheet Ext.ActionSheet
+ audio Ext.Audio
+ button Ext.Button
+ image Ext.Img
+ label Ext.Label
+ loadmask Ext.LoadMask
+ panel Ext.Panel
+ segmentedbutton Ext.SegmentedButton
+ sheet Ext.Sheet
+ spacer Ext.Spacer
+ titlebar Ext.TitleBar
+ toolbar Ext.Toolbar
+ video Ext.Video
+ carousel Ext.carousel.Carousel
+ navigationview Ext.navigation.View
+ datepicker Ext.picker.Date
+ picker Ext.picker.Picker
+ slider Ext.slider.Slider
+ thumb Ext.slider.Thumb
+ tabpanel Ext.tab.Panel
+ viewport Ext.viewport.Default
+
+ DataView Components
+ ---------------------------------------------
+ dataview Ext.dataview.DataView
+ list Ext.dataview.List
+ nestedlist Ext.dataview.NestedList
+
+ Form Components
+ ---------------------------------------------
+ checkboxfield Ext.field.Checkbox
+ datepickerfield Ext.field.DatePicker
+ emailfield Ext.field.Email
+ hiddenfield Ext.field.Hidden
+ numberfield Ext.field.Number
+ passwordfield Ext.field.Password
+ radiofield Ext.field.Radio
+ searchfield Ext.field.Search
+ selectfield Ext.field.Select
+ sliderfield Ext.field.Slider
+ spinnerfield Ext.field.Spinner
+ textfield Ext.field.Text
+ textareafield Ext.field.TextArea
+ togglefield Ext.field.Toggle
+ urlfield Ext.field.Url
+ fieldset Ext.form.FieldSet
+ formpanel Ext.form.Panel
+ *
+ *
+ * ## Configuring Components
+ *
+ * Whenever you create a new Component you can pass in configuration options. All of the
+ * configurations for a given Component are listed in the "Config options" section of its class
+ * docs page. You can pass in any number of configuration options when you instantiate the
+ * Component, and modify any of them at any point later. For example, we can easily modify the
+ * {@link Ext.Panel#html html content} of a Panel after creating it:
+ *
+ * @example
+ * // we can configure the HTML when we instantiate the Component
+ * var panel = Ext.create('Ext.Panel', {
+ * fullscreen: true,
+ * html: 'This is a Panel'
+ * });
+ *
+ * // we can update the HTML later using the setHtml method:
+ * panel.setHtml('Some new HTML');
+ *
+ * // we can retrieve the current HTML using the getHtml method:
+ * Ext.Msg.alert(panel.getHtml()); // displays "Some new HTML"
+ *
+ * Every config has a getter method and a setter method - these are automatically generated and
+ * always follow the same pattern. For example, a config called `html` will receive `getHtml` and
+ * `setHtml` methods, a config called `defaultType` will receive `getDefaultType` and
+ * `setDefaultType` methods, and so on.
+ *
+ * @disable {DuplicateAlternateClassName}
+ */
+Ext.define('Ext.Component', {
+ extend: Ext.Widget,
+ mixins: [
+ Ext.state.Stateful
+ ],
+ // @override Ext.Widget
+ alternateClassName: [
+ 'Ext.lib.Component',
+ 'Ext.Gadget'
+ ],
+ /**
+ * @cfg {String} xtype
+ * The `xtype` configuration option can be used to optimize Component creation and rendering.
+ * It serves as a shortcut to the full component name. For example, the component
+ * `Ext.button.Button` has an xtype of `button`.
+ *
+ * You can define your own xtype on a custom {@link Ext.Component component} like so:
+ *
+ * Ext.define('PressMeButton', {
+ * extend: 'Ext.button.Button',
+ * xtype: 'pressmebutton',
+ * text: 'Press Me'
+ * });
+ *
+ * Any Component can be created implicitly as an object config with an xtype specified,
+ * allowing it to be declared and passed into the rendering pipeline without actually being
+ * instantiated as an object. Not only is rendering deferred, but the actual creation of the
+ * object itself is also deferred, saving memory and resources until they are actually needed.
+ * In complex, nested layouts containing many Components, this can make a noticeable
+ * improvement in performance.
+ *
+ * // Explicit creation of contained Components:
+ * var panel = new Ext.Panel({
+ * // ...
+ * items: [
+ * Ext.create('Ext.button.Button', {
+ * text: 'OK'
+ * })
+ * ]
+ * });
+ *
+ * // Implicit creation using xtype:
+ * var panel = new Ext.Panel({
+ * // ...
+ * items: [{
+ * xtype: 'button',
+ * text: 'OK'
+ * }]
+ * });
+ *
+ * In the first example, the button will always be created immediately during the panel's
+ * initialization. With many added Components, this approach could potentially slow the
+ * rendering of the page. In the second example, the button will not be created or rendered
+ * until the panel is actually displayed in the browser. If the panel is never displayed
+ * (for example, if it is a tab that remains hidden) then the button will never be created and
+ * will never consume any resources whatsoever.
+ */
+ xtype: 'component',
+ cachedConfig: {
+ /**
+ * @cfg {Number/String} padding
+ * The padding to use on this Component. Can be specified as a number (in which
+ * case all edges get the same padding) or a CSS string like '5 10 10 10'
+ * @accessor
+ */
+ padding: null,
+ /**
+ * @cfg {Number} tabIndex
+ * DOM tabIndex attribute for this component's
+ * {@link #focusEl}.
+ */
+ tabIndex: null
+ },
+ eventedConfig: {
+ /**
+ * @cfg {Number/String} left
+ * The absolute left position of this Component; must be a valid CSS length value,
+ * e.g: `300`, `100px`, `30%`, etc. Explicitly setting this value will make this Component
+ * become 'positioned', which means it will no longer participate in the layout of the
+ * Container that it resides in.
+ * @accessor
+ * @evented
+ */
+ left: null,
+ /**
+ * @cfg {Number/String} top
+ * The absolute top position of this Component; must be a valid CSS length value,
+ * e.g: `300`, `100px`, `30%`, etc. Explicitly setting this value will make this Component
+ * become 'positioned', which means it will no longer participate in the layout of the
+ * Container that it resides in.
+ * @accessor
+ * @evented
+ */
+ top: null,
+ /**
+ * @cfg {Number/String} right
+ * The absolute right position of this Component; must be a valid CSS length value,
+ * e.g: `300`, `100px`, `30%`, etc. Explicitly setting this value will make this Component
+ * become 'positioned', which means it will no longer participate in the layout of the
+ * Container that it resides in.
+ * @accessor
+ * @evented
+ */
+ right: null,
+ /**
+ * @cfg {Number/String} bottom
+ * The absolute bottom position of this Component; must be a valid CSS length value,
+ * e.g: `300`, `100px`, `30%`, etc. Explicitly setting this value will make this Component
+ * become 'positioned', which means it will no longer participate in the layout of the
+ * Container that it resides in.
+ * @accessor
+ * @evented
+ */
+ bottom: null,
+ /**
+ * @cfg {Number/String} minWidth
+ * The minimum width of this Component; must be a valid CSS length value,
+ * e.g: `300`, `100px`, `30%`, etc. If set to `auto`, it will set the width to `null`
+ * meaning it will have its own natural size.
+ * @accessor
+ * @evented
+ */
+ minWidth: null,
+ /**
+ * @cfg {Number/String} minHeight
+ * The minimum height of this Component; must be a valid CSS length value,
+ * e.g: `300`, `100px`, `30%`, etc. If set to `auto`, it will set the width to `null`
+ * meaning it will have its own natural size.
+ * @accessor
+ * @evented
+ */
+ minHeight: null,
+ /**
+ * @cfg {Number/String} maxWidth
+ * The maximum width of this Component; must be a valid CSS length value,
+ * e.g: `300`, `100px`, `30%`, etc. If set to `auto`, it will set the width to `null`
+ * meaning it will have its own natural size. Note that this config will not apply if the
+ * Component is 'positioned' (absolutely positioned or centered)
+ * @accessor
+ * @evented
+ */
+ maxWidth: null,
+ /**
+ * @cfg {Number/String} maxHeight
+ * The maximum height of this Component; must be a valid CSS length value,
+ * e.g: `300`, `100px`, `30%`, etc. If set to `auto`, it will set the width to `null`
+ * meaning it will have its own natural size. Note that this config will not apply if the
+ * Component is 'positioned' (absolutely positioned or centered)
+ * @accessor
+ * @evented
+ */
+ maxHeight: null,
+ /**
+ * @cfg {Boolean/String/Object} scrollable
+ * Configuration options to make this Component scrollable. Acceptable values are:
+ *
+ * - `true` to enable auto scrolling.
+ * - `false` (or `null`) to disable scrolling - this is the default.
+ * - `x` or `horizontal` to enable horizontal scrolling only
+ * - `y` or `vertical` to enable vertical scrolling only
+ *
+ * Also accepts a configuration object for a `{@link Ext.scroll.Scroller}` if
+ * if advanced configuration is needed.
+ *
+ * The getter for this config returns the {@link Ext.scroll.Scroller Scroller}
+ * instance. You can use the Scroller API to read or manipulate the scroll position:
+ *
+ * // scrolls the component to 5 on the x axis and 10 on the y axis
+ * component.getScrollable().scrollTo(5, 10);
+ *
+ * @accessor
+ * @evented
+ */
+ scrollable: null,
+ /**
+ * @cfg {String} docked
+ * The dock position of this component in its container. Can be `left`, `top`, `right` or
+ * `bottom`.
+ *
+ * __Notes__
+ *
+ * You must use a HTML5 doctype for {@link #docked} `bottom` to work. To do this, simply
+ * add the following code to the HTML file:
+ *
+ *
+ *
+ * So your index.html file should look a little like this:
+ *
+ *
+ *
+ *
+ * MY application title
+ * ...
+ *
+ * @accessor
+ * @evented
+ */
+ docked: null,
+ /**
+ * @cfg {Boolean} [centered=false]
+ * Configure this as `true` to have this Component centered within its Container.
+ * Setting this value to `true` will make this Component become 'positioned', which means
+ * it will no longer participate in the layout of the Container that it resides in.
+ * @accessor
+ * @evented
+ */
+ centered: {
+ lazy: true,
+ $value: null
+ }
+ },
+ config: {
+ /**
+ * @cfg {Boolean} displayed
+ * Set to `true` to call `show` and `false` to call `hide`. Unlike the `hidden`
+ * config, changing this config will potentially involve animations to show or
+ * hide the component.
+ * @since 6.5.0
+ */
+ displayed: null,
+ /**
+ * @cfg {String/Ext.Element/HTMLElement} html
+ * Optional HTML content to render inside this Component, or a reference to an
+ * existing element on the page.
+ * @accessor
+ */
+ html: null,
+ // @cmd-auto-dependency { defaultType: "Ext.drag.Source" }
+ /**
+ * @cfg {Boolean/Object/Ext.drag.Source} draggable
+ * Set to `true` to allow this component to be dragged. This can also be the config
+ * object for the `Ext.drag.Source` that will manage the drag.
+ */
+ draggable: null,
+ /**
+ * @cfg {Number} zIndex
+ * The z-index to give this Component when it is rendered.
+ *
+ * Not valid for {@link #cfg-floated} Components. The Z ordering of {@link #cfg-floated}
+ * Components is managed by ordering of the DOM elements.
+ * @accessor
+ */
+ zIndex: null,
+ /**
+ * @cfg {String/String[]/Ext.Template/Ext.XTemplate[]} tpl
+ * A {@link String}, {@link Ext.Template}, {@link Ext.XTemplate} or an {@link Array} of
+ * strings to form an {@link Ext.XTemplate}. Used in conjunction with the {@link #data}
+ * and {@link #tplWriteMode} configurations.
+ *
+ * __Note__
+ * The {@link #data} configuration _must_ be set for any content to be shown in the
+ * component when using this configuration.
+ * @accessor
+ */
+ tpl: null,
+ /**
+ * @cfg {String/Mixed} enterAnimation
+ * Animation effect to apply when the Component is being shown. Typically you want to use
+ * an inbound animation type such as 'fadeIn' or 'slideIn'.
+ * @deprecated 2.0.0 Please use {@link #showAnimation} instead.
+ * @accessor
+ */
+ enterAnimation: null,
+ /**
+ * @cfg {String/Mixed} exitAnimation
+ * Animation effect to apply when the Component is being hidden.
+ * @deprecated 2.0.0 Please use {@link #hideAnimation} instead. Typically you want to use
+ * an outbound animation type such as 'fadeOut' or 'slideOut'.
+ * @accessor
+ */
+ exitAnimation: null,
+ /**
+ * @cfg {String/Mixed} showAnimation
+ * Animation effect to apply when the Component is being shown. Typically you want to use
+ * an inbound animation type such as 'fadeIn' or 'slideIn'. For more animations, check the
+ * {@link Ext.fx.Animation#type} config.
+ * @accessor
+ */
+ showAnimation: null,
+ /**
+ * @cfg {String/Mixed} hideAnimation
+ * Animation effect to apply when the Component is being hidden. Typically you want to use
+ * an outbound animation type such as 'fadeOut' or 'slideOut'. For more animations, check
+ * the {@link Ext.fx.Animation#type} config.
+ * @accessor
+ */
+ hideAnimation: null,
+ /**
+ * @cfg {String} tplWriteMode
+ * The Ext.(X)Template method to use when updating the content area of the
+ * Component.
+ *
+ * Valid modes are:
+ *
+ * - append
+ * - insertAfter
+ * - insertBefore
+ * - insertFirst
+ * - overwrite
+ * @accessor
+ */
+ tplWriteMode: 'overwrite',
+ /**
+ * @cfg {Object} data
+ * The initial set of data to apply to the `{@link #tpl}` to
+ * update the content area of the Component.
+ *
+ * **Note:** Data will be appended to any existing data.
+ *
+ * @accessor
+ */
+ data: null,
+ /**
+ * @cfg {Ext.Element/HTMLElement/String} contentEl
+ * The configured element will automatically be added as the content of this
+ * component. When you pass a string, we expect it to be an element id. If the
+ * content element is hidden, we will automatically show it.
+ * @accessor
+ */
+ contentEl: null,
+ /**
+ * @cfg {Ext.data.Model} record
+ * A model instance which updates the Component's html based on it's tpl. Similar
+ * to the data configuration, but tied to to a record to make allow dynamic
+ * updates. This must be a model instance and not a configuration of one.
+ * @accessor
+ */
+ record: null,
+ // @cmd-auto-dependency {defaultType: "Ext.tip.ToolTip"}
+ /**
+ * @cfg {String/Object} tooltip
+ * The tooltip for this component - can be a string to be used as innerHTML
+ * (html tags are accepted) or {@link Ext.tip.ToolTip} config object.
+ *
+ * The default behavior is to use a shared tip instance. The tooltip configuration is
+ * registered with the {@link Ext.tip.Manager}. To enable this, your application can set
+ * the {@link Ext.app.Application#quickTips} config, or an instance of the
+ * {@link Ext.tip.Manager} may be created manually.
+ *
+ * To force a unique tooltip instance to be created, specify `autoCreate: true` on this
+ * configuration.
+ *
+ * Configuring this with `autoHide: false` implies `autoCreate: true` so that the desired
+ * persistent behavior can be obtained with other targets still showing the singleton
+ * instance.
+ */
+ tooltip: null,
+ /**
+ * @cfg {Boolean} axisLock
+ * If `true`, then, when {@link #showBy} or {@link #alignTo} fallback on
+ * constraint violation only takes place along the major align axis.
+ *
+ * That is, if alignment `"l-r"` is being used, and `axisLock: true` is used,
+ * then if constraints fail, only fallback to `"r-l"` is considered.
+ */
+ axisLock: null,
+ // @cmd-auto-dependency {defaultType: "Ext.Mask"}
+ /**
+ * @cfg {Boolean} modal
+ * `true` to make this Component modal. This will create a mask underneath the
+ * Component that covers its parent and does not allow the user to interact with
+ * any other Components until this Component is dismissed.
+ * @accessor
+ */
+ modal: {
+ lazy: true,
+ $value: null
+ },
+ /**
+ * @cfg {Boolean} hideOnMaskTap
+ * When using a {@link #cfg!modal} Component, setting this to `true` will hide
+ * the modal mask and the Container when the mask is tapped on.
+ * @accessor
+ */
+ hideOnMaskTap: null,
+ /**
+ * @cfg {Number} [weight=0]
+ * This value controls this item's order in a {@link Ext.Container#cfg!weighted weighted}
+ * {@link Ext.Container container} (see {@link #cfg!parent}).
+ *
+ * Lower values gravitate towards the start of the container - the top in vertical layouts,
+ * the locale start side in horizontal layouts.
+ */
+ weight: null,
+ /**
+ * @cfg {Boolean/String/Object} [userSelectable=false]
+ *
+ * Set to true to allow users to select text within this component.
+ *
+ * Can also be any valid value for the CSS3
+ * [user-select](https://developer.mozilla.org/en-US/docs/Web/CSS/user-select) property.
+ *
+ * A value of true implies `auto`, while false implies `none`.
+ *
+ * May also be an object keyed by child element name.
+ *
+ * By default, the user cannot click+drag+select text/elements of the UI. Applications may
+ * want to enable user selection for specific DOM elements, such as the bodyElement of
+ * a component used as a tab panel. The tab and tab text would not be user selectable in
+ * this example, but the content area when the tab is selected would.
+ *
+ * userSelectable: {
+ * element: true, // allow the element to be user selectable
+ * bodyElement: true // allow the component's body element to be user selectable
+ * }
+ *
+ * @since 6.5.1
+ */
+ userSelectable: null
+ },
+ /**
+ * @cfg {Boolean} modelValidation
+ * This config enables binding to your `{@link Ext.data.Model#validators}`. This
+ * is only processed by form fields (e.g., `Ext.field.*`) at present, however, this
+ * setting is inherited and so can be set on a parent container.
+ *
+ * When set to `true` by a component (or by an ancestor container), the `validators`
+ * of for any {@Ext.data.Model record} fields will be used wherever the `value` is
+ * bound to such data fields.
+ *
+ * While this config can be set arbitrarily high in the component hierarchy, doing
+ * so can create a lot overhead if most of your form fields do not actually rely on
+ * `validators` in your data model.
+ *
+ * Using this setting for a form that is bound to an `Ext.data.Model` might look
+ * like this:
+ *
+ * {
+ * xtype: 'panel',
+ * modelValidation: true,
+ * items: [{
+ * xtype: 'textfield',
+ * bind: '{theUser.firstName}'
+ * },{
+ * xtype: 'textfield',
+ * bind: '{theUser.lastName}'
+ * },{
+ * xtype: 'textfield',
+ * bind: '{theUser.phoneNumber}'
+ * },{
+ * xtype: 'textfield',
+ * bind: '{theUser.email}'
+ * }]
+ * }
+ * @since 6.5.0
+ */
+ modelValidation: null,
+ /**
+ * @event beforeshow
+ * Fires before the Component is shown. Show may be vetoed by returning `false` from a handler.
+ * @param {Ext.Component} sender The component firing this event.
+ */
+ /**
+ * @event show
+ * Fires whenever the Component is shown
+ * @param {Ext.Component} sender The component firing this event.
+ */
+ /**
+ * @event beforehide
+ * Fires before the Component is hidden. Hide may be vetoed by returning `false` from a handler.
+ * @param {Ext.Component} sender The component firing this event.
+ */
+ /**
+ * @event hide
+ * Fires whenever the Component is hidden
+ * @param {Ext.Component} sender The component firing this event.
+ */
+ /**
+ * @event fullscreen
+ * Fires whenever a Component with the fullscreen config is instantiated
+ * @param {Ext.Component} sender The component firing this event.
+ */
+ /**
+ * @event floatingchange
+ * Fires whenever there is a change in the positioned status of a component
+ * @param {Ext.Component} sender The component firing this event.
+ * @param {Boolean} positioned The component's new positioned state. This becomes
+ * `true` is a component is positioned using the {@link #cfg-top}, {@link #cfg-right},
+ * {@link #cfg-bottom} or {@link #cfg-left} configs.
+ * @deprecated 6.2.0 Use {@link #positionedchange} instead
+ */
+ /**
+ * @event positionedchange
+ * Fires whenever there is a change in the positioned status of a component
+ * @param {Ext.Component} sender The component firing this event.
+ * @param {Boolean} positioned The component's new positioned state. This becomes
+ * `true` is a component is positioned using the {@link #cfg-top}, {@link #cfg-right},
+ * {@link #cfg-bottom} or {@link #cfg-left} configs.
+ */
+ /**
+ * @event destroy
+ * Fires when the component is destroyed
+ */
+ /**
+ * @event beforeorientationchange
+ * Fires before orientation changes.
+ * @removed 2.0.0 This event is now only available `onBefore` the Viewport's
+ * {@link Ext.Viewport#orientationchange}
+ */
+ /**
+ * @event orientationchange
+ * Fires when orientation changes.
+ * @removed 2.0.0 This event is now only available on the Viewport's
+ * {@link Ext.Viewport#orientationchange}
+ */
+ /**
+ * @event initialize
+ * Fires when the component has been initialized
+ * @param {Ext.Component} sender The component firing this event.
+ */
+ /**
+ * @event painted
+ * @inheritdoc Ext.dom.Element#painted
+ * @param {Ext.Component} sender The component firing this event.
+ * @param {Ext.Element} element The component's outer element (this.element)
+ */
+ /**
+ * @event erased
+ * Fires when the component is no longer displayed in the DOM. Listening to this event will
+ * degrade performance not recommend for general use.
+ * @param {Ext.Component} sender The component firing this event.
+ */
+ /**
+ * @event resize
+ * @inheritdoc Ext.dom.Element#resize
+ * @param {Ext.Element} element The component's outer element (this.element).
+ * @param {Object} info The component's new size parameters.
+ */
+ /**
+ * @event added
+ * Fires after a Component had been added to a Container.
+ * @param {Ext.Component} sender The component firing this event.
+ * @param {Ext.Container} container Parent Container
+ * @param {Number} index The index of the item within the Container.
+ */
+ /**
+ * @event removed
+ * Fires when a component is removed from a Container
+ * @param {Ext.Component} sender The component firing this event.
+ * @param {Ext.Container} container Container which holds the component
+ * @param {Number} index The index of the item that was removed.
+ */
+ /**
+ * @event moved
+ * Fires when a component si moved within its Container.
+ * @param {Ext.Component} sender The component firing this event.
+ * @param {Ext.Container} container Container which holds the component
+ * @param {Number} toIndex The new index of the item.
+ * @param {Number} fromIndex The old index of the item.
+ */
+ /**
+ * @property {Boolean} rendered
+ * @readonly
+ * The rendered flag is set when a widget is inserted into the document for the first time.
+ *
+ * Note that this is a one-way operation. The first time a widget is inserted into the
+ * document, this flag is set, and it is never unset.
+ */
+ /**
+ * @property defaultBindProperty
+ * @inheritdoc
+ */
+ defaultBindProperty: 'html',
+ /**
+ * @private
+ */
+ isComponent: true,
+ /**
+ * @private
+ */
+ positioned: false,
+ /**
+ * @private
+ */
+ rendered: false,
+ /**
+ * @private
+ */
+ activeAnimation: null,
+ /**
+ * @readonly
+ * @private
+ */
+ dockPositions: {
+ top: true,
+ right: true,
+ bottom: true,
+ left: true
+ },
+ bodyElement: null,
+ /**
+ * @property classCls
+ * @inheritdoc
+ */
+ classCls: Ext.baseCSSPrefix + 'component',
+ /**
+ * @property floatingCls
+ * @inheritdoc
+ */
+ floatingCls: Ext.baseCSSPrefix + 'floating',
+ hiddenCls: Ext.baseCSSPrefix + 'hidden',
+ _scrollableCfg: {
+ x: {
+ x: true,
+ y: false
+ },
+ y: {
+ x: false,
+ y: true
+ },
+ horizontal: {
+ x: true,
+ y: false
+ },
+ vertical: {
+ x: false,
+ y: true
+ },
+ both: {
+ x: true,
+ y: true
+ },
+ 'true': {
+ x: true,
+ y: true
+ },
+ 'false': {
+ x: false,
+ y: false
+ }
+ },
+ statics: {
+ /**
+ * Find the Widget or Component to which the given event/element belongs.
+ *
+ * @param {Ext.event.Event/Ext.dom.Element/HTMLElement} el The element or event
+ * from which to start to find an owning Component.
+ * @param {Ext.dom.Element/HTMLElement} [limit] The element at which to stop upward
+ * searching for an owning Component, or the number of Components to traverse
+ * before giving up. Defaults to the document's HTML element.
+ * @param {String} [selector] An optional {@link Ext.ComponentQuery} selector to
+ * filter the target.
+ * @return {Ext.Component} Component, or null
+ *
+ * @since 6.5.0
+ */
+ from: function(el, limit, selector) {
+ return Ext.ComponentManager.from(el, limit, selector);
+ },
+ /**
+ * Find the Widget or Component to which the given Element belongs.
+ *
+ * @param {Ext.dom.Element/HTMLElement} el The element from which to start to find an
+ * owning Component.
+ * @param {Ext.dom.Element/HTMLElement} [limit] The element at which to stop upward
+ * searching for an owning Component, or the number of Components to traverse before giving
+ * up. Defaults to the document's HTML element.
+ * @param {String} [selector] An optional {@link Ext.ComponentQuery} selector to filter the
+ * target.
+ * @return {Ext.Component/null} Component, or null
+ *
+ * @deprecated 6.5.0 Use {@link Ext.Component#method!from} instead.
+ * @since 6.0.1
+ */
+ fromElement: function(el, limit, selector) {
+ return Ext.ComponentManager.from(el, limit, selector);
+ }
+ },
+ initialConfig: null,
+ $initParent: null,
+ /**
+ * @private
+ */
+ userSelectableClsMap: {
+ "true": Ext.baseCSSPrefix + 'user-selectable-auto',
+ "false": Ext.baseCSSPrefix + 'user-selectable-none',
+ all: Ext.baseCSSPrefix + 'user-selectable-all',
+ auto: Ext.baseCSSPrefix + 'user-selectable-auto',
+ text: Ext.baseCSSPrefix + 'user-selectable-text',
+ none: Ext.baseCSSPrefix + 'user-selectable-none'
+ },
+ /**
+ * Creates new Component.
+ * @param {Object} config The standard configuration object.
+ */
+ constructor: function(config) {
+ var me = this,
+ // eslint-disable-next-line dot-notation
+ VP = Ext['Viewport'],
+ // There is similar code in widget, however we
+ // want to defer rendering until the component has been initialized
+ renderTo = config && config.renderTo,
+ hasListeners, hasResize, el;
+ me.lastSize = {};
+ me.onInitializedListeners = [];
+ if (config) {
+ me.initialConfig = config;
+ // We need to copy this over here and not rely on initConfig to do so since
+ // configs (esp cached configs like "ui") can be set() prior to copying of
+ // such properties.
+ me.$initParent = config.$initParent;
+ }
+ if (renderTo) {
+ config = Ext.apply({}, config);
+ delete config.renderTo;
+ }
+ me.callParent([
+ config
+ ]);
+ el = me.el;
+ // Start with the assumption that we are at the root of the component/container
+ // hierarchy unless we begin with an upward ownership link.
+ // rootCls will be removed when we are added to a container
+ if (!me.getRefOwner()) {
+ el.addCls(me.rootCls);
+ }
+ me.refreshPositioned = me.doRefreshPositioned;
+ if (me.refreshPositionedOnInitialized) {
+ me.refreshPositioned();
+ }
+ me.initialize();
+ me.triggerInitialized();
+ if (me.isCentered()) {
+ // re-center after container border, etc., may have resized us.
+ me.center();
+ }
+ hasListeners = me.hasListeners;
+ hasResize = hasListeners.resize;
+ // Add these after initializing to prevent the monitoring elements
+ // from being added too early. If they get added early, they end up
+ // as part of the renderTemplate.
+ if (me.hasListeners.painted) {
+ el.on('painted', 'handleElementPainted', me);
+ }
+ if ((hasResize || me.onResize) && !me.isViewport) {
+ if (!hasResize) {
+ // If we don't have a resize listener, bump the increment
+ // so that the resize listener on the underlying element isn't unbound
+ // if resize listeners are reduced to zero
+ hasListeners._incr_('resize');
+ }
+ el.on({
+ scope: me,
+ resize: 'handleElementResize',
+ priority: 1000
+ });
+ }
+ /**
+ * Force the component to take up 100% width and height available, by adding it
+ * to {@link Ext.Viewport}.
+ * @cfg {Boolean} fullscreen
+ */
+ if (me.fullscreen && VP) {
+ VP.setItemFullScreen(me);
+ }
+ me.fireEvent('initialize', me);
+ if (renderTo) {
+ me.setRenderTo(renderTo);
+ }
+ },
+ beforeInitConfig: function(config) {
+ this.beforeInitialize.apply(this, arguments);
+ },
+ /**
+ * @method
+ * @private
+ */
+ beforeInitialize: Ext.emptyFn,
+ /**
+ * @method
+ * Allows addition of behavior to the rendering phase.
+ * @protected
+ * @template
+ */
+ initialize: Ext.emptyFn,
+ /**
+ * Center this {@link #cfg-floated} or {@link #isPositioned positioned} Component in its parent.
+ * @return {Ext.Component} this
+ */
+ center: function() {
+ var me = this,
+ parent = me.getParent(),
+ parentBox, translateXY, xy, size;
+ // Sometimes the center method will be pre-processed by the component#show,
+ // when this happens me.isVisible() will be false but the element is already
+ // visible, so we should check for me.el.isVisible() here.
+ if ((!parent || parent.rendered) && me.shouldRecenter()) {
+ translateXY = !!parent;
+ parent = parent ? parent.bodyElement : Ext.getBody();
+ parentBox = parent.getConstrainRegion();
+ size = me.measure();
+ xy = [
+ (parentBox.getWidth() - size.width) / 2,
+ (parentBox.getHeight() - size.height) / 2
+ ];
+ me.needsCenter = false;
+ me._centering = true;
+ if (me.getFloated()) {
+ if (translateXY) {
+ xy = parent.reverseTranslateXY(xy);
+ }
+ // local to page
+ me.setXY(xy);
+ } else {
+ me.setLeft(xy[0]);
+ me.setTop(xy[1]);
+ }
+ me._centering = false;
+ } else {
+ me.needsCenter = true;
+ }
+ return me;
+ },
+ shouldRecenter: function() {
+ // We should center if we are rendered, not in the middle of an animated show, and the
+ // element is measurable. It's only not measurable if it's hidden by display.
+ return this.rendered && !this.$isShowing && (this.el.isVisible() || this.el.getVisibilityMode() !== Ext.Element.DISPLAY);
+ },
+ /**
+ * Returns the topmost modal floated component (other then this one).
+ * @private
+ */
+ getModalSibling: function() {
+ var me = this,
+ floatRoot = Ext.getFloatRoot().dom,
+ parentWrap, parentContainer, childNodes, c, i;
+ // Loop upwards through floatParents to find the next modal down the stack
+ for (parentWrap = me.floatParentNode && me.floatParentNode.dom; parentWrap; parentWrap = (parentWrap === floatRoot || parentContainer.getRelative()) ? null : parentWrap.parentNode) {
+ parentContainer = Ext.fly(parentWrap).getData().component;
+ childNodes = parentWrap.childNodes;
+ // Loop backwards examining floatWraps in this floatParent to find a visible modal
+ // that isn't this
+ for (i = childNodes ? childNodes.length : 0; i-- > 0; ) {
+ c = Ext.fly(childNodes[i]);
+ if (c.hasCls(me.floatWrapCls)) {
+ c = c.getData().component;
+ if (c && c !== me && c.isVisible() && c.getModal()) {
+ return c;
+ }
+ }
+ }
+ }
+ return null;
+ },
+ /**
+ * Invoked when a scroll is initiated on this component via its {@link #scrollable scroller}.
+ * @method onScrollStart
+ * @param {Number} x The current x position
+ * @param {Number} y The current y position
+ * @template
+ * @protected
+ */
+ /**
+ * Invoked when this component is scrolled via its {@link #scrollable scroller}.
+ * @method onScrollMove
+ * @param {Number} x The current x position
+ * @param {Number} y The current y position
+ * @template
+ * @protected
+ */
+ /**
+ * Invoked when a scroll operation is completed via this component's
+ * {@link #scrollable scroller}.
+ * @method onScrollEnd
+ * @param {Number} x The current x position
+ * @param {Number} y The current y position
+ * @template
+ * @protected
+ */
+ /**
+ * @private
+ */
+ triggerInitialized: function() {
+ var listeners = this.onInitializedListeners,
+ ln = listeners.length,
+ listener, fn, scope, args, i;
+ if (!this.initialized) {
+ this.initialized = true;
+ if (ln > 0) {
+ for (i = 0; i < ln; i++) {
+ listener = listeners[i];
+ fn = listener.fn;
+ scope = listener.scope;
+ args = listener.args;
+ if (typeof fn === 'string') {
+ scope[fn].apply(scope, args);
+ } else {
+ fn.apply(scope, args);
+ }
+ }
+ listeners.length = 0;
+ }
+ }
+ },
+ /**
+ * @private
+ */
+ onInitialized: function(fn, scope, args) {
+ var me = this,
+ listeners = me.onInitializedListeners;
+ if (!scope) {
+ scope = me;
+ }
+ if (args) {
+ args = args.slice();
+ args.unshift(me);
+ } else {
+ args = [
+ me
+ ];
+ }
+ if (me.initialized) {
+ if (typeof fn === 'string') {
+ scope[fn].apply(scope, args);
+ } else {
+ fn.apply(scope, args);
+ }
+ } else {
+ listeners.push({
+ fn: fn,
+ scope: scope,
+ args: args
+ });
+ }
+ },
+ initElement: function() {
+ var me = this;
+ me.callParent();
+ if (!me.bodyElement) {
+ me.bodyElement = me.element;
+ }
+ // alias for backward compatibility with v < 6.5
+ me.innerElement = me.innerElement || me.bodyElement;
+ },
+ /**
+ * Called by `getInherited` to initialize the inheritedState the first time it is requested.
+ * @protected
+ */
+ initInheritedState: function(inheritedState) {
+ var me = this;
+ // TODO
+ // if (me.hidden) {
+ // inheritedState.hidden = true;
+ // }
+ if (me.modelValidation !== null) {
+ inheritedState.modelValidation = me.modelValidation;
+ }
+ me.callParent([
+ inheritedState
+ ]);
+ },
+ applyScrollable: function(scrollable, oldScrollable) {
+ var me = this;
+ if ((typeof scrollable === 'boolean') || (typeof scrollable === 'string')) {
+ if (!me._scrollableCfg[scrollable]) {
+ Ext.raise("'" + scrollable + "' is not a valid value for 'scrollable'");
+ }
+ scrollable = me._scrollableCfg[scrollable];
+ }
+ return Ext.Factory.scroller.update(oldScrollable, scrollable, this, 'createScrollable');
+ },
+ applyHidden: function(hidden) {
+ if (!hidden && this.isConfiguring && this.getFloated()) {
+ this.preprocessShow();
+ }
+ return !!hidden;
+ },
+ createScrollable: function(defaults) {
+ return Ext.apply({
+ component: this,
+ element: this.getScrollerTarget()
+ }, defaults);
+ },
+ getScrollerTarget: function() {
+ return this.bodyElement;
+ },
+ /**
+ * This method is required by the Scroller to return the scrollable client region
+ * @return {Ext.util.Region} The scrolling viewport region.
+ * @private
+ */
+ getScrollableClientRegion: function() {
+ var me = this,
+ region = me.getScrollerTarget().getClientRegion(),
+ scroller = me.getScrollable(),
+ scrollbarSize;
+ if (scroller && scroller.isVirtualScroller) {
+ scrollbarSize = scroller.getScrollbarSize();
+ region.adjust(0, 0, -scrollbarSize.height, -scrollbarSize.width);
+ }
+ return region;
+ },
+ updatePadding: function(padding) {
+ this.bodyElement.setPadding(padding);
+ },
+ updateWeight: function(weight, oldWeight) {
+ var me = this,
+ owner = !me.isConfiguring && me.getRefOwner();
+ // We want a simply-named property.
+ me.weight = weight;
+ // Inform the owning Container which might want to reorder the DOM
+ if (owner && owner.onItemWeightChange) {
+ owner.onItemWeightChange(me, weight, oldWeight);
+ }
+ },
+ applyContentEl: function(contentEl) {
+ if (contentEl) {
+ return Ext.get(contentEl);
+ }
+ },
+ updateContentEl: function(newContentEl, oldContentEl) {
+ if (oldContentEl) {
+ oldContentEl.hide();
+ Ext.getBody().append(oldContentEl);
+ }
+ if (newContentEl) {
+ this.setHtml(newContentEl.dom);
+ newContentEl.show();
+ }
+ },
+ createTranslatable: function(config) {
+ var me = this,
+ ret = me.callParent([
+ config
+ ]);
+ if (config && !config.type && me.getFloated()) {
+ ret.type = 'csstransform';
+ }
+ return ret;
+ },
+ /**
+ * @private
+ * @return {boolean}
+ */
+ isCentered: function() {
+ return Boolean(this.getCentered());
+ },
+ isPositioned: function() {
+ return this.positioned;
+ },
+ isDocked: function() {
+ return Boolean(this.getDocked());
+ },
+ applyTop: function(top) {
+ return this.filterLengthValue(top);
+ },
+ applyRight: function(right) {
+ return this.filterLengthValue(right);
+ },
+ applyBottom: function(bottom) {
+ return this.filterLengthValue(bottom);
+ },
+ applyLeft: function(left) {
+ return this.filterLengthValue(left);
+ },
+ applyMinWidth: function(width) {
+ return this.filterLengthValue(width);
+ },
+ applyMinHeight: function(height) {
+ return this.filterLengthValue(height);
+ },
+ applyMaxWidth: function(width) {
+ return this.filterLengthValue(width);
+ },
+ applyMaxHeight: function(height) {
+ return this.filterLengthValue(height);
+ },
+ updateTop: function(top) {
+ if (!this.$updatingXY) {
+ this.element.setTop(top);
+ }
+ this.refreshPositioned();
+ },
+ updateRight: function(right) {
+ if (!this.$updatingXY) {
+ this.element.setRight(right);
+ }
+ this.refreshPositioned();
+ },
+ updateBottom: function(bottom) {
+ if (!this.$updatingXY) {
+ this.element.setBottom(bottom);
+ }
+ this.refreshPositioned();
+ },
+ updateLeft: function(left) {
+ if (!this.$updatingXY) {
+ this.element.setLeft(left);
+ }
+ this.refreshPositioned();
+ },
+ /**
+ * @method
+ * Optional template method. If implemented, this is called *asynchronously* after a browser
+ * layout caused by a component resize. This may be triggered for any or several of the
+ * following reasons:
+ * - Programmatic changes to {@link #cfg-width} or {@link #cfg-height} configs.
+ * - Setting the {@link #cfg-flex} config when the owning layout is {@link Ext.layout.Box}.
+ * - Setting {@link #cfg-minHeight}, {@link #cfg-maxHeight}, {@link #cfg-minWidth}
+ * or {@link #cfg-maxWidth}.
+ * - Changing device orientation.
+ * - Changing the browser viewport size.
+ * - Any resize caused by browser layout recalculation which may be caused by content
+ * size changes or application of default browser layout rules.
+ * @param {Number} width The new width.
+ * @param {Number} height The new height.
+ * @param {Number} oldWidth The previous width.
+ * @param {Number} oldHeight The previous height.
+ * @protected
+ * @template
+ */
+ onResize: null,
+ updateMinWidth: function(width) {
+ this.element.setMinWidth(width);
+ },
+ updateMinHeight: function(height) {
+ this.element.setMinHeight(height);
+ },
+ updateMaxWidth: function(width) {
+ this.element.setMaxWidth(width);
+ },
+ updateMaxHeight: function(height) {
+ this.element.setMaxHeight(height);
+ },
+ /**
+ * @private
+ * @param {Boolean} centered
+ * @return {Boolean}
+ */
+ applyCentered: function(centered) {
+ var me = this,
+ doCenter = me.getLeft() === null && me.getRight() === null && me.getTop() === null && me.getBottom() === null;
+ // We can only center if the CSS top/right/bottom/left properties are not being used.
+ if (doCenter) {
+ return !!centered;
+ }
+ },
+ updateCentered: function(centered) {
+ var me = this,
+ resizeParent;
+ if (me.getFloated()) {
+ if (centered) {
+ me.center();
+ if (!me.centerResizeListener && !me.needsCenter) {
+ resizeParent = me.floatParentNode;
+ resizeParent = (resizeParent === Ext.floatRoot) ? Ext : resizeParent;
+ me.centerResizeListener = resizeParent.on({
+ resize: 'center',
+ scope: me,
+ destroyable: true
+ });
+ }
+ } else {
+ me.centerResizeListener = Ext.destroy(me.centerResizeListener);
+ }
+ } else {
+ me.el.toggleCls(me.floatingCls, centered);
+ if (centered) {
+ me.refreshInnerState = Ext.emptyFn;
+ if (me.isContainer && (!me.isWidthed() || !me.isHeighted())) {
+ me.setAutoSize(true);
+ }
+ if (me.isPositioned()) {
+ me.resetPositioned();
+ }
+ if (me.isDocked()) {
+ me.setDocked(false);
+ }
+ me.setIsInner(false);
+ delete me.refreshInnerState;
+ } else {
+ me.refreshInnerState();
+ }
+ }
+ },
+ applyDocked: function(docked) {
+ var me = this;
+ if (!docked) {
+ return null;
+ }
+ if (!/^(top|right|bottom|left)$/.test(docked)) {
+ Ext.Logger.error("Invalid docking position of '" + docked.position + "', must be either 'top', 'right', 'bottom', " + "'left' or `null` (for no docking)", me);
+ return;
+ }
+ me.refreshInnerState = Ext.emptyFn;
+ if (me.isPositioned()) {
+ me.resetPositioned();
+ }
+ if (me.isCentered()) {
+ me.setCentered(false);
+ }
+ me.setIsInner(false);
+ delete me.refreshInnerState;
+ return docked;
+ },
+ getDisplayed: function() {
+ return !this.getHidden();
+ },
+ setDisplayed: function(displayed) {
+ var me = this,
+ hidden = me.getHidden() !== false;
+ if (displayed === hidden) {
+ me._displayed = displayed;
+ me.updateDisplayed(displayed, !displayed);
+ }
+ return me;
+ },
+ updateDisplayed: function(displayed) {
+ this[displayed ? 'show' : 'hide']();
+ },
+ updateDocked: function(docked, oldDocked) {
+ var me = this;
+ if (!me.isConfiguring) {
+ me.fireEvent('afterdockedchange', me, docked, oldDocked);
+ if (!docked) {
+ me.refreshInnerState();
+ }
+ }
+ },
+ updateUserSelectable: function(newSelectable, oldSelectable) {
+ var me = this,
+ map = me.userSelectableClsMap,
+ el = me.el,
+ name, childEl;
+ if (typeof oldSelectable === 'boolean' || typeof oldSelectable === 'string') {
+ el.removeCls(map[oldSelectable]);
+ } else {
+ for (name in oldSelectable) {
+ childEl = me[name];
+ if (!childEl || !childEl.isElement) {
+ Ext.raise('Element not found: "' + name + '"');
+ }
+ childEl.removeCls(map[oldSelectable[name]]);
+ }
+ }
+ if (typeof newSelectable === 'boolean' || typeof newSelectable === 'string') {
+ el.addCls(map[newSelectable]);
+ } else {
+ for (name in newSelectable) {
+ childEl = me[name];
+ if (!childEl || !childEl.isElement) {
+ Ext.raise('Element not found: "' + name + '"');
+ }
+ childEl.addCls(map[newSelectable[name]]);
+ }
+ }
+ },
+ /**
+ * Resets {@link #top}, {@link #right}, {@link #bottom} and {@link #left} configurations to
+ * `null`, which will cause this component to stop being 'positioned' and to take its place in
+ * its owning container's layout.
+ */
+ resetPositioned: function() {
+ var me = this;
+ me.setTop(null);
+ me.setRight(null);
+ me.setBottom(null);
+ me.setLeft(null);
+ },
+ refreshPositioned: function() {
+ this.refreshPositionedOnInitialized = true;
+ },
+ doRefreshPositioned: function() {
+ var me = this,
+ // We are positioned if we are *not* floated, and any of the
+ // positioning configs are non-null.
+ positioned = !me.getConfig('floated', false, true) && (me.getTop() !== null || me.getBottom() !== null || me.getRight() !== null || me.getLeft() !== null);
+ if (positioned !== this.positioned) {
+ me.positioned = positioned;
+ if (positioned) {
+ me.refreshInnerState = Ext.emptyFn;
+ if (me.isContainer && (!me.isWidthed() || !me.isHeighted())) {
+ me.setAutoSize(true);
+ }
+ if (me.isCentered()) {
+ me.setCentered(false);
+ }
+ if (me.isDocked()) {
+ me.setDocked(false);
+ }
+ me.setIsInner(false);
+ delete me.refreshInnerState;
+ }
+ me.element.toggleCls(me.floatingCls, positioned);
+ if (me.initialized) {
+ me.fireEvent('floatingchange', me, positioned);
+ me.fireEvent('positionedchange', me, positioned);
+ }
+ if (!positioned) {
+ me.refreshInnerState();
+ }
+ }
+ },
+ applyZIndex: function(zIndex) {
+ if (!zIndex && zIndex !== 0) {
+ zIndex = null;
+ }
+ if (zIndex !== null) {
+ zIndex = Number(zIndex);
+ if (isNaN(zIndex)) {
+ zIndex = null;
+ }
+ }
+ return zIndex;
+ },
+ updateZIndex: function(zIndex) {
+ var element = this.element,
+ modal = !this.getFloated() && this.getModal(),
+ domStyle;
+ if (element && !element.destroyed) {
+ domStyle = element.dom.style;
+ if (zIndex !== null) {
+ domStyle.setProperty('z-index', zIndex, 'important');
+ } else {
+ domStyle.removeProperty('z-index');
+ }
+ }
+ if (modal && !modal.destroyed) {
+ modal.setZIndex(zIndex - 1);
+ }
+ },
+ getInnerHtmlElement: function() {
+ var me = this,
+ innerHtmlElement = me.innerHtmlElement;
+ if (!innerHtmlElement || !innerHtmlElement.dom || !innerHtmlElement.dom.parentNode) {
+ me.innerHtmlElement = innerHtmlElement = Ext.Element.create({
+ cls: Ext.baseCSSPrefix + 'innerhtml'
+ });
+ me.getRenderTarget().appendChild(innerHtmlElement);
+ }
+ return innerHtmlElement;
+ },
+ updateHtml: function(html) {
+ var innerHtmlElement;
+ if (!this.destroyed) {
+ innerHtmlElement = this.getInnerHtmlElement();
+ if (Ext.isElement(html)) {
+ innerHtmlElement.setHtml('');
+ innerHtmlElement.append(html);
+ } else {
+ innerHtmlElement.setHtml(html);
+ }
+ }
+ },
+ updateHidden: function(hidden, oldHidden) {
+ var me = this,
+ element = me.renderElement,
+ modal = me.getModal(),
+ name;
+ if (me.rendered) {
+ if (modal && !modal.destroyed) {
+ if (me.getFloated()) {
+ if (hidden) {
+ // Hiding a modal must move the modal back to below the next
+ // highest visible modal
+ modal = me.getModalSibling();
+ if (modal) {
+ modal.showModalMask();
+ } else {
+ me.hideModalMask();
+ }
+ } else {
+ me.showModalMask();
+ }
+ } else {
+ if (modal !== true) {
+ modal.setZIndex(me.getZIndex() - 1);
+ if (modal.getHidden() !== hidden) {
+ modal.setHidden(hidden);
+ }
+ }
+ }
+ }
+ if (!me.destroying && element && !element.destroyed) {
+ element.toggleCls(me.hiddenCls, hidden);
+ }
+ me.callParent([
+ hidden,
+ oldHidden
+ ]);
+ } else {
+ element.toggleCls(me.hiddenCls, hidden);
+ me.callParent([
+ hidden,
+ oldHidden
+ ]);
+ }
+ // Updating to hidden during config should not fire events
+ if (!me.isConfiguring && !me.destroying) {
+ name = hidden ? 'hide' : 'show';
+ if (me.hasListeners[name]) {
+ me.fireEvent(name, me);
+ }
+ me[hidden ? 'afterHide' : 'afterShow'](me);
+ }
+ },
+ /**
+ * Hides this Component optionally using an animation.
+ * @param {Object/Boolean} [animation] You can specify an animation here or a bool to use the
+ * {@link #hideAnimation} config.
+ * @return {Ext.Component}
+ * @chainable
+ */
+ hide: function(animation) {
+ var me = this,
+ activeAnim = me.activeAnimation,
+ modal;
+ if (me.isVisible()) {
+ // Allow veto of hide.
+ if (me.hasListeners.beforehide && me.fireEvent('beforehide', me) === false) {
+ return;
+ }
+ if (me.beforeHide() === false) {
+ return;
+ }
+ me.viewportResizeListener = Ext.destroy(me.viewportResizeListener);
+ me.setCurrentAlignmentInfo(null);
+ if (activeAnim) {
+ activeAnim.on({
+ animationend: function() {
+ me.hide(animation);
+ },
+ single: true
+ });
+ return me;
+ }
+ if (!me.getHidden()) {
+ // Not passed, or truthy but not an object means use the default animation
+ if (animation === undefined || (animation && !Ext.isObject(animation))) {
+ animation = me.getHideAnimation();
+ }
+ if (animation) {
+ me.on({
+ beforehiddenchange: 'onBeforeHiddenChange',
+ scope: me,
+ single: true,
+ args: [
+ animation
+ ]
+ });
+ }
+ me.setHidden(true);
+ }
+ // Hide the owned modal mask which positioned Components use to
+ // implement modality.
+ // Floated Components share a single modal mask that is owned by
+ // their floatParent.
+ if (!me.getFloated()) {
+ modal = me.getModal();
+ if (modal && modal !== true && !modal.destroyed) {
+ modal.setHidden(true);
+ }
+ }
+ } else {
+ me.setHidden(true);
+ }
+ return me;
+ },
+ /**
+ * @private
+ * This is the private method to ensure everything is set up for showing.
+ * This is called by both show and showBy to set a component up.
+ * the user-facing method is beforeShow, this setup must have
+ * happened by the time that is called.
+ */
+ preprocessShow: function(component, alignment, options) {
+ var me = this,
+ hideMode = me.getHideMode(),
+ hidden;
+ // This is needed if we are going through a setHidden(false) during configuration.
+ //
+ // This ensures that the configurations are initialized. There will be "initGetters" set
+ // which are like a mousetrap. They actually set the configuration, and then delete
+ // themselves leaving the getter from the prototype to be the visible getter.
+ if (me.isContainer) {
+ me.getItems();
+ }
+ me.getHtml();
+ if (me.getFloated()) {
+ // Only need to show to get measurements if we're hidden by display:none.
+ hidden = (!hideMode || hideMode === 'display') && me.getHidden();
+ // The following operations require that the component be
+ // temporarily visible for measurement purposes.
+ if (me.rendered) {
+ if (hidden) {
+ me.setVisibility(true);
+ me._hidden = false;
+ }
+ } else // An instantiated, but not yet rendered floated.
+ // It will still be wrapped in its documentFragment.
+ // Insert it into the global floatRoot and make it visible if necessary.
+ {
+ hidden = hidden !== false;
+ me.findFloatParent(hidden);
+ }
+ // Note: If aligning, we have to ensure the final shape is set by flushing
+ // though any ViewModel data.
+ if (component) {
+ me.notifyIf();
+ me.alignTo(component, alignment, options);
+ } else {
+ if (me.isCentered()) {
+ me.notifyIf();
+ // We need to set the viewport resize listener to keep centered
+ me.updateCentered(true);
+ } else {
+ me.syncXYPosition();
+ }
+ }
+ if (me.getModal()) {
+ me.showModalMask();
+ }
+ if (me.getToFrontOnShow()) {
+ me.toFront();
+ } else {
+ me.syncAlwaysOnTop();
+ }
+ if (hidden) {
+ me.setVisibility(false);
+ me._hidden = true;
+ }
+ }
+ },
+ /**
+ * Shows this component by another component. If you specify no alignment, it will automatically
+ * position this component relative to the reference component depending upon the `alignment`
+ * parameter.
+ *
+ * The `alignment` parameter describes the edge-to-edge alignment of *this* component
+ * with the target. It can be specified thus:
+ *
+ * - **Blank**: Defaults to positioning where the component will fit trying `'l-r?'`,
+ * `'r-l?'`, `'b-t?'` then `'t-b?'` in that order.
+ * - **Two anchors**: If two values from the table below are passed separated by a dash,
+ * the first value is used as the element's anchor point, and the second value is
+ * used as the target's anchor point.
+ * - **Two edge/offset descriptors:** An edge/offset descriptor is an edge initial
+ * (`t`/`r`/`b`/`l`) followed by a percentage along that side. This describes a
+ * point to align with a similar point in the target. So `'t0-b0'` would be
+ * the same as `'tl-bl'`, `'l0-r50'` would place the top left corner of this item
+ * halfway down the right edge of the target item. This allows more flexibility
+ * and also describes which two edges are considered adjacent when positioning a tip pointer.
+ *
+ * Following are all of the supported predefined anchor positions:
+ *
+ * Value Description
+ * ----- -----------------------------
+ * tl The top left corner
+ * t The center of the top edge
+ * tr The top right corner
+ * l The center of the left edge
+ * c The center
+ * r The center of the right edge
+ * bl The bottom left corner
+ * b The center of the bottom edge
+ * br The bottom right corner
+ *
+ * You can put a '?' at the end of the alignment string to constrain the positioned element to
+ * the {@link Ext.Viewport Viewport}. The element will attempt to align as specified, but the
+ * position will be adjusted to constrain to the viewport if necessary. Note that the element
+ * being aligned might be swapped to align to a different position than that specified in order
+ * to enforce the viewport constraints.
+ *
+ * Example Usage:
+ *
+ * // show `panel` by `button` using the default positioning (auto fit)
+ * panel.showBy(button);
+ *
+ * // align the top left corner of `panel` with the top right corner of `button`
+ * // (constrained to viewport)
+ * panel.showBy(button, "tl-tr?");
+ *
+ * // align the bottom right corner of `panel` with the center left edge of `button`
+ * // (not constrained by viewport)
+ * panel.showBy(button, "br-cl");
+ *
+ * // align the center of panel with the bottom left corner of button and
+ * // adjust the x position by -6 pixels (and the y position by 0)
+ * panel.showBy(button, "c-bl", [-6, 0]);
+ *
+ * // align the 25% point on the bottom edge of this panel
+ * // with the 75% point on the top edge of button.
+ * panel.showBy(button, 'b25-t75');
+ *
+ * @param {Ext.Component} component The target component to show this component by.
+ * @param {String} [alignment] The alignment string, eg: `'tl-bl'`.
+ * @param {Object/Array} [options] An object containing options for the
+ * {@link Ext.util.Region#alignTo} method, if an Array is used, it will be assumed to be the
+ * offset.
+ */
+ showBy: function(component, alignment, options) {
+ var me = this,
+ offset;
+ if (Ext.isArray(options)) {
+ offset = options;
+ options = {
+ offset: offset
+ };
+ }
+ // We may be called while visible, just for repositioning.
+ if (me.isVisible()) {
+ me.alignTo(component, alignment, options);
+ } else {
+ // Correct the component display type for being aligned.
+ // Component must become floated in time for the preprocessShow
+ // machinery to get it into the DOM.
+ if (!me.getFloated()) {
+ if (!me.getParent()) {
+ me.setFloated(true);
+ } else {
+ me.positioned = true;
+ }
+ }
+ // Cache the alignment options for any realign call which might happen on
+ // viewport resize or configuration change.
+ me.alignToArgs = [
+ component,
+ alignment,
+ options
+ ];
+ me.show({
+ // To allow the show method to differentiate between this object as an
+ // animation config and "options", it must contain an animation property.
+ // True means use the default animation
+ animation: true,
+ alignment: {
+ component: component,
+ alignment: alignment,
+ options: options
+ }
+ });
+ }
+ },
+ /**
+ * Shows this component optionally using an animation.
+ * @param {Object/Boolean} [animation] You can specify an animation here or a bool to
+ * use the {@link #showAnimation} config.
+ * @param {Object} [options] (private)
+ * @param {Object/Boolean} [options.animation] You can specify an animation here or a
+ * bool to use the {@link #showAnimation} config.
+ * @param {Object} [options.alignment] An object containing alignment details.
+ * @param {Object} [options.alignment.component] The target component to show this component by.
+ * @param {Object} [options.alignment.alignment] The alignment string, eg: `'tl-bl'`.
+ * @param {Object} [options.alignment.options] An object containing options for the
+ * {@link Ext.util.Region#alignTo} method.
+ * @return {Ext.Component}
+ * @chainable
+ */
+ show: function(animation, options) {
+ var me = this,
+ hidden = me.getHidden(),
+ floated = me.getFloated(),
+ alignment, modal;
+ // One arg signature if options has an animation property.
+ // Animation argument can be a Boolean.
+ if (animation && typeof animation === 'object' && 'animation' in animation) {
+ options = animation;
+ animation = options.animation;
+ } else // Optional animation and options args
+ {
+ options = Ext.apply({
+ animation: animation
+ }, options);
+ }
+ // Allow event listener veto of show or modify options.
+ if (me.hasListeners.beforeshow && me.fireEvent('beforeshow', me, options) === false) {
+ return false;
+ }
+ // Allow template method to veto show or modify options.
+ if (me.beforeShow(options) === false) {
+ return;
+ }
+ // Prepare the component for a potentially animated show.
+ // If floated, it must be layed out, and the final alignment calculated.
+ alignment = options.alignment || {};
+ me.preprocessShow(alignment.component, alignment.alignment, alignment.options);
+ if (me.activeAnimation) {
+ // If the activeAnimation is not a show, reschedule this show when it's done.
+ // If it is a show, this show will be ignored.
+ if (!me.$isShowing) {
+ me.activeAnimation.on({
+ animationend: function() {
+ if (!me.destroying && !me.destroyed) {
+ me.show(animation, options);
+ }
+ },
+ single: true
+ });
+ }
+ return me;
+ }
+ // flag that we need to wait for the animation to complete before doing another show
+ // this fixes a race condition where show() is called twice, the 2nd time before the
+ // first's animation is completed
+ if (hidden || hidden === null) {
+ // Not passed, or truthy but not an object means use the default animation
+ if (animation === undefined || (animation && !Ext.isObject(animation))) {
+ animation = me.getShowAnimation();
+ }
+ if (animation && !me.isConfiguring) {
+ me.on({
+ beforehiddenchange: 'onBeforeHiddenChange',
+ scope: me,
+ single: true,
+ args: [
+ animation
+ ]
+ });
+ }
+ me.setHidden(false);
+ }
+ // Show the owned modal mask which positioned Components use to
+ // implement modality.
+ // Floated Components share a single modal mask that is owned by
+ // their floatParent.
+ if (!floated) {
+ modal = me.getModal();
+ if (modal && modal.setHidden) {
+ modal.setHidden(false);
+ }
+ }
+ return me;
+ },
+ onAnimationStart: function(hidden, animation, data) {
+ var me = this,
+ element = me.element,
+ fromTransform = data.from.transform,
+ toTransform = data.to.transform;
+ // If we are showing, show the element just as the animation begins
+ if (!hidden) {
+ me.renderElement.show();
+ element.removeCls(me.hiddenCls);
+ if (me.needsCenter) {
+ me.center();
+ }
+ }
+ me.$isShowing = true;
+ // If the animation is not controlling the position, clear the positioning
+ // properties of the transform data
+ if (me.getFloated() && fromTransform && toTransform && !(fromTransform.translateX | toTransform.translateX | fromTransform.translateY | toTransform.translateY)) {
+ fromTransform.translateX = toTransform.translateX = null;
+ fromTransform.translateY = toTransform.translateY = null;
+ }
+ },
+ onBeforeHiddenChange: function(animation, component, hidden, oldHidden, controller) {
+ var me = this;
+ if (animation && (!hidden || (hidden && me.isPainted()))) {
+ if (!animation.isAnimation) {
+ animation = hidden ? me.createHideAnimation(animation) : me.createShowAnimation(animation);
+ animation = Ext.Factory.animation(animation);
+ }
+ me.activeAnimation = animation;
+ animation.on({
+ animationstart: 'onAnimationStart',
+ scope: me,
+ single: true,
+ args: [
+ hidden
+ ]
+ });
+ if (!Ext.isEmpty(hidden)) {
+ animation.setOnEnd(function() {
+ me.activeAnimation = null;
+ me.$isShowing = false;
+ // Even if we are destroying we need to resume the controller
+ // to be able to call updateHidden which hides the float mask
+ // if we are modal.
+ if (!me.destroyed) {
+ controller.resume();
+ // Could have been destroyed in controller.resume()
+ if (me.destroying || me.destroyed) {
+ return;
+ }
+ if (me.getFloated()) {
+ me.syncXYPosition();
+ }
+ }
+ });
+ // Force the state to visibility if we're being shown.
+ if (!hidden) {
+ me._hidden = false;
+ }
+ controller.pause();
+ }
+ Ext.Animator.run(animation);
+ }
+ },
+ /**
+ * @private
+ */
+ setVisibility: function(isVisible) {
+ this.renderElement.setVisible(isVisible);
+ },
+ /**
+ * @private
+ */
+ isRendered: function() {
+ return this.rendered;
+ },
+ /**
+ * @private
+ */
+ isPainted: function() {
+ return this.renderElement.isPainted();
+ },
+ /**
+ * @private
+ */
+ applyTpl: function(tpl) {
+ return Ext.XTemplate.get(tpl);
+ },
+ updateTpl: function(tpl) {
+ if (!this.isConfiguring) {
+ if (tpl) {
+ this.doUpdateTpl();
+ } else {
+ this.getInnerHtmlElement().setHtml('');
+ }
+ }
+ },
+ applyData: function(data) {
+ if (Ext.isObject(data)) {
+ return Ext.apply({}, data);
+ } else if (!data) {
+ data = {};
+ }
+ return data;
+ },
+ /**
+ * @private
+ */
+ updateData: function(newData) {
+ var me = this;
+ if (newData) {
+ me.doUpdateTpl(newData);
+ /**
+ * @event updatedata
+ * Fires whenever the data of the component is updated
+ * @param {Ext.Component} sender The component firing this event.
+ * @param {Object} newData The new data
+ */
+ if (!me.isConfiguring) {
+ me.fireEvent('updatedata', me, newData);
+ }
+ }
+ },
+ applyRecord: function(config) {
+ if (config && Ext.isObject(config) && config.isModel) {
+ return config;
+ }
+ return null;
+ },
+ updateRecord: function(newRecord, oldRecord) {
+ var me = this;
+ if (oldRecord) {
+ oldRecord.unjoin(me);
+ }
+ if (!newRecord) {
+ me.updateData('');
+ } else {
+ newRecord.join(me);
+ me.doUpdateTpl(newRecord.getData(true));
+ }
+ },
+ /**
+ * @private
+ * Used to handle joining of a record to a tpl
+ */
+ afterEdit: function() {
+ this.updateRecord(this.getRecord());
+ },
+ /**
+ * @private
+ * Used to handle joining of a record to a tpl
+ */
+ afterErase: function() {
+ this.setRecord(null);
+ },
+ /**
+ * Returns this Component's xtype hierarchy as a slash-delimited string. For a list of all
+ * available xtypes, see the {@link Ext.Component} header.
+ *
+ * __Note:__ If using your own subclasses, be aware that a Component must register its own xtype
+ * to participate in determination of inherited xtypes.
+ *
+ * Example usage:
+ *
+ * var t = new Ext.field.Text();
+ * alert(t.getXTypes()); // alerts 'component/field/textfield'
+ *
+ * @return {String} The xtype hierarchy string.
+ */
+ getXTypes: function() {
+ return this.xtypesChain.join('/');
+ },
+ /**
+ * @method initDragConstraints
+ * This method is called when a drag is initializing. This method should adjust the
+ * drag constraints to ensure all drag movements are properly restricted. See
+ * {@link Ext.drag.Source#constrain}.
+ * @param {Ext.drag.Source} draggable
+ * @template
+ * @since 6.5.0
+ */
+ applyDraggable: function(draggable, existing) {
+ if (existing) {
+ if (draggable) {
+ existing.setConfig(draggable);
+ } else {
+ existing.destroy();
+ }
+ } else if (draggable) {
+ draggable = this.createDraggable(draggable);
+ draggable = new Ext.drag.Source(draggable);
+ if (this.initDragConstraints) {
+ draggable.on('initdragconstraints', 'initDragConstraints', this);
+ }
+ }
+ return draggable;
+ },
+ createDraggable: function(draggable) {
+ var me = this,
+ listeners = draggable.listeners;
+ draggable = Ext.apply({
+ autoDestroy: false,
+ component: me,
+ ownerCmp: me,
+ local: true
+ }, draggable);
+ // The resolveListenerScope will handle this case, but this saves many
+ // function calls during mousemove...
+ if (listeners && listeners.scope === 'this') {
+ draggable.listeners = listeners = Ext.apply({}, listeners);
+ listeners.scope = me;
+ }
+ return draggable;
+ },
+ updateDraggable: function(dragger, existing) {
+ if (existing) {
+ if (dragger) {
+ existing.setConfig(dragger);
+ } else {
+ existing.destroy();
+ }
+ }
+ },
+ onModalMaskTap: function(e) {
+ if (this.getHideOnMaskTap()) {
+ this.hide();
+ }
+ },
+ translateAxis: function(axis, value, animation) {
+ var x, y;
+ if (axis === 'x') {
+ x = value;
+ } else {
+ y = value;
+ }
+ return this.translate(x, y, animation);
+ },
+ /**
+ * @private
+ */
+ alignTo: function(component, alignment, options) {
+ var me = this;
+ // Components maintain alignment upon viewport resize
+ if (!me.viewportResizeListener) {
+ me.viewportResizeListener = Ext.on({
+ resize: 'onViewportResize',
+ scope: me,
+ destroyable: true
+ });
+ }
+ me.aligning = true;
+ me.callParent([
+ component,
+ alignment,
+ Ext.apply({
+ axisLock: me.getAxisLock()
+ }, options)
+ ]);
+ me.aligning = false;
+ },
+ onViewportResize: function() {
+ if (this.isVisible()) {
+ this.realign();
+ }
+ },
+ /**
+ * Displays component at specific xy position.
+ * A floating component (like a menu) is positioned relative to its ownerCt if any.
+ * Useful for popping up a context menu:
+ *
+ * listeners: {
+ * itemcontextmenu: function(view, record, item, index, event, options) {
+ * Ext.create('Ext.menu.Menu', {
+ * width: 100,
+ * height: 100,
+ * margin: '0 0 10 0',
+ * items: [{
+ * text: 'regular item 1'
+ * },{
+ * text: 'regular item 2'
+ * },{
+ * text: 'regular item 3'
+ * }]
+ * }).showAt(event.getXY());
+ * }
+ * }
+ *
+ * @param {Number/Number[]/Object} x The new x position or array of `[x,y]`, or an
+ * object `{x:10, y:10}`.
+ * @param {Number} [y] The new y position.
+ * @return {Ext.Component} this
+ */
+ showAt: function(x, y) /* , animate TODO: Animate to position? */
+ {
+ var me = this;
+ if (me.getFloated() || me.isPositioned()) {
+ if (arguments.length === 1) {
+ if (x.x) {
+ y = x.y;
+ x = x.x;
+ } else {
+ y = x[1];
+ x = x[0];
+ }
+ }
+ me.show();
+ if (me.isPositioned()) {
+ me.setLeft(x);
+ me.setTop(y);
+ } else {
+ me.setX(x);
+ me.setY(y);
+ }
+ }
+ return me;
+ },
+ onAdded: function(parent, instanced) {
+ var me = this,
+ modal;
+ me.callParent([
+ parent,
+ instanced
+ ]);
+ if (!me.getFloated()) {
+ modal = me.getModal();
+ if (modal) {
+ parent.insertBefore(modal, me);
+ modal.setZIndex(me.getZIndex() - 1);
+ }
+ }
+ me.el.removeCls(me.rootCls);
+ },
+ onRemoved: function(destroying) {
+ if (!destroying) {
+ this.el.addCls(this.rootCls);
+ }
+ this.callParent([
+ destroying
+ ]);
+ },
+ applyTooltip: function(tooltip) {
+ if (tooltip) {
+ if (tooltip.isInstance) {
+ tooltip.setTarget(this);
+ return tooltip;
+ } else if (typeof tooltip === 'string') {
+ tooltip = {
+ html: tooltip
+ };
+ } else {
+ tooltip = Ext.merge({}, tooltip);
+ }
+ // autocreate means we own an instance.
+ // autoHide: false implies that too, otherwise
+ // any other component's use of the singleton would defeat autoHide: false
+ if (tooltip.autoCreate || tooltip.autoHide === false) {
+ delete tooltip.autoCreate;
+ tooltip.target = this;
+ tooltip.xtype = tooltip.xtype || 'tooltip';
+ tooltip = Ext.create(tooltip);
+ } else {
+ delete tooltip.xtype;
+ }
+ }
+ return tooltip;
+ },
+ updateTooltip: function(tooltip, oldTooltip) {
+ var el = this.el,
+ data, manager, target, tip;
+ if (oldTooltip) {
+ if (oldTooltip.isInstance) {
+ Ext.destroy(oldTooltip);
+ } else {
+ target = Ext.fly(oldTooltip.target);
+ data = target && target.peekData();
+ if (data) {
+ delete data.qtip;
+ }
+ }
+ }
+ if (tooltip && !tooltip.isInstance) {
+ el.getData().qtip = tooltip;
+ // eslint-disable-next-line dot-notation
+ manager = Ext['tip'];
+ manager = manager && manager.Manager;
+ manager = manager && manager.instance;
+ if (manager) {
+ tip = manager.tip;
+ if (tip.currentTarget.dom === el.dom) {
+ manager.onBeforeShow(tip);
+ }
+ }
+ }
+ },
+ applyModal: function(modal, currentModal) {
+ var isVisible;
+ if (this.getFloated()) {
+ return !!modal;
+ }
+ isVisible = this.isVisible();
+ if (modal === false) {
+ modal = true;
+ isVisible = false;
+ }
+ // eslint-disable-next-line dot-notation
+ currentModal = Ext.factory(modal, Ext['Mask'], typeof currentModal === 'boolean' ? null : currentModal);
+ if (currentModal) {
+ currentModal.setVisibility(isVisible);
+ currentModal.on('tap', 'onModalMaskTap', this);
+ }
+ return currentModal;
+ },
+ updateModal: function(modal) {
+ var me = this,
+ parent = me.getParent(),
+ positionEl = (me.floatWrap || me.element).dom,
+ topModal;
+ if (me.getFloated()) {
+ if (modal) {
+ if (me.isVisible() && !positionEl.nextSibling) {
+ me.showModalMask();
+ }
+ } else {
+ topModal = me.getModalSibling();
+ if (topModal) {
+ topModal.showModalMask();
+ } else {
+ // Modal mask must now drop to below the next modal
+ // below us, or hide.
+ me.hideModalMask();
+ }
+ }
+ } else {
+ if (parent) {
+ if (modal) {
+ parent.insertBefore(modal, me);
+ modal.setZIndex(me.getZIndex() - 1);
+ } else {
+ parent.remove(modal);
+ }
+ }
+ }
+ },
+ applyHideAnimation: function(hideAnimation, oldHideAnimation) {
+ return Ext.Factory.animation.update(oldHideAnimation, hideAnimation, this, 'createHideAnimation');
+ },
+ createHideAnimation: function(defaults) {
+ return Ext.apply({
+ type: 'fadeOut',
+ element: this.element
+ }, defaults);
+ },
+ applyShowAnimation: function(showAnimation, oldShowAnimation) {
+ return Ext.Factory.animation.update(oldShowAnimation, showAnimation, this, 'createShowAnimation');
+ },
+ createShowAnimation: function(defaults) {
+ return Ext.apply({
+ type: 'fadeIn',
+ element: this.element
+ }, defaults);
+ },
+ /**
+ * Perform the actual destruction sequence. This is the method to override in your
+ * subclasses to add steps specific to the destruction of custom Component.
+ *
+ * If the Component is currently added to a Container it will first be removed
+ * from that Container. All {@link Ext.Element} references are also deleted and
+ * the Component is de-registered from {@link Ext.ComponentManager}.
+ *
+ * As a rule of thumb, subclasses should destroy their child Components, Elements,
+ * and/or other objects before calling parent method. Any object references will be
+ * nulled after this method has finished, to prevent the possibility of memory leaks.
+ *
+ * @since 6.2.0
+ */
+ doDestroy: function() {
+ var me = this,
+ sibling;
+ // Ensure focus is moved somewhere predictable.
+ // Ensure modal mask is hidden or moved to below next highest visible floated sibling.
+ if (me.isVisible()) {
+ me.revertFocus();
+ if (me.getModal() && me.getFloated()) {
+ // Destroying a modal must move the modal back to below the next
+ // highest visible modal
+ sibling = me.getModalSibling();
+ if (sibling) {
+ sibling.showModalMask();
+ } else {
+ me.hideModalMask();
+ }
+ }
+ }
+ if (me.hasListeners.destroy) {
+ me.fireEvent('destroy', me);
+ }
+ me.destroyMembers('modal', 'innerHtmlElement', 'scrollerElement', 'scrollable', // animations should also be destroyed
+ 'showAnimation', // destroy of the hide animation calls the 'updateHidden'
+ 'hideAnimation', 'centerResizeListener', 'visibleListener');
+ me.setPlugins(null);
+ me.setRecord(null);
+ me.setTooltip(null);
+ me.callParent();
+ },
+ privates: {
+ // Eventually this flag should default to true, however there are many
+ // consequences that may flow on from this. For now, retain the old behaviour until
+ // we can come up with a more consistent approach.
+ preciseWidth: false,
+ clearWhenVisible: function(fn) {
+ var me = this,
+ pending = me.pendingVisible;
+ if (pending) {
+ delete pending[fn];
+ if (Ext.Object.isEmpty(pending)) {
+ me.pendingVisible = null;
+ me.visibleListener = Ext.destroy(me.visibleListener);
+ }
+ }
+ },
+ convertToLocalXY: function(xy) {
+ var me = this,
+ pageXY = me.element.getXY(),
+ x = 0,
+ y = 0;
+ if (me.isPositioned()) {
+ x = me.getLeft() || 0;
+ y = me.getTop() || 0;
+ } else {
+ x = me.getX() || 0;
+ y = me.getY() || 0;
+ }
+ return [
+ xy[0] - pageXY[0] + x,
+ xy[1] - pageXY[1] + y
+ ];
+ },
+ doAddListener: function(name, fn, scope, options, order, caller, manager) {
+ var me = this,
+ el = me.element;
+ // Add element listeners which will fire the component level event
+ // only when the first expensive listener is added. Don't attach
+ // listener until initialized to prevent resize/painted monitor
+ // elements from being included as part of our renderTemplate
+ if (me.initialized) {
+ if (name === 'painted') {
+ if (!me.hasListeners.painted) {
+ el.on('painted', 'handleElementPainted', me);
+ }
+ } else if (name === 'resize' && !me.isViewport) {
+ if (!me.hasListeners.resize) {
+ el.on({
+ scope: me,
+ resize: 'handleElementResize',
+ priority: 1000
+ });
+ }
+ }
+ }
+ return me.callParent([
+ name,
+ fn,
+ scope,
+ options,
+ order,
+ caller,
+ manager
+ ]);
+ },
+ doRemoveListener: function(name, fn, scope) {
+ var me = this,
+ el = me.element,
+ ret = me.callParent([
+ name,
+ fn,
+ scope
+ ]);
+ // Remove expensive element level listeners if nobody is listening
+ // to the component level event.
+ if (ret && me.initialized) {
+ if (name === 'painted') {
+ if (!me.hasListeners.painted) {
+ el.un('painted', 'handleElementPainted', me);
+ }
+ } else if (name === 'resize' && !me.isViewport) {
+ if (!me.hasListeners.resize) {
+ el.un('resize', 'handleElementResize', me);
+ }
+ }
+ }
+ return ret;
+ },
+ doUpdateTpl: function(data) {
+ var me = this,
+ tpl = me.getTpl(),
+ writeMode;
+ if (tpl) {
+ writeMode = me.getTplWriteMode();
+ data = data || me.getData() || {};
+ tpl[writeMode](me.getInnerHtmlElement(), data);
+ }
+ },
+ /**
+ * Returns the element into which the html content and items should be rendered.
+ * This defaults to the `bodyElement` but derived classes can override this method
+ * to use a different element.
+ *
+ * For containers his only applies to `inner` items, not `docked` items. The
+ * `positioned` items are rendered to the element returned by the
+ * {@link Ext.Container#getPositionedItemTarget method.
+ * @return {Ext.dom.Element}
+ * @private
+ * @since 6.5.0
+ */
+ getRenderTarget: function() {
+ var scroller = this.getScrollable();
+ return (scroller && scroller.isVirtualScroller) ? scroller.getInnerElement() : this.bodyElement;
+ },
+ handleElementPainted: function(el) {
+ this.fireEvent('painted', this, el);
+ },
+ handleElementResize: function(el, info) {
+ var me = this,
+ ceil = Math.ceil,
+ lastSize = me.lastSize,
+ oldWidth = lastSize.width || null,
+ oldHeight = lastSize.height || null,
+ w, h;
+ // Only fire the event if we have actually resized.
+ if (info.flag) {
+ w = info.width;
+ h = info.height;
+ // This will be the case if we're not visible, so don't fire
+ // resize events in that case
+ if (w === 0 && h === 0) {
+ return;
+ }
+ if (!me.preciseWidth) {
+ w = ceil(w);
+ h = ceil(h);
+ }
+ lastSize.width = w;
+ lastSize.height = h;
+ if (me.onResize) {
+ me.onResize(w, h, oldWidth, oldHeight, info);
+ }
+ if (me.isCentered()) {
+ me.center();
+ }
+ me.fireEvent('resize', this, w, h, oldWidth, oldHeight, info);
+ }
+ },
+ handleGlobalShow: function(c) {
+ var me = this;
+ if (me.isVisible(true) && (c === me || me.isDescendantOf(c))) {
+ me.runWhenVisible();
+ }
+ },
+ hasHiddenContent: function() {
+ return this.getHidden();
+ },
+ runWhenVisible: function() {
+ var me = this,
+ pending = me.pendingVisible,
+ key;
+ me.pendingVisible = null;
+ me.visibleListener = Ext.destroy(me.visibleListener);
+ for (key in pending) {
+ me[key].apply(me, pending[key]);
+ }
+ },
+ /**
+ * Queue a function to run when the component is visible & painted. If those conditions
+ * are met, the function will execute immediately, otherwise it will wait until it is
+ * visible and painted.
+ *
+ * @param {String} fn The function to execute on this component.
+ * @param {Object[]} [args] The arguments to pass.
+ * @return {Boolean} `true` if the function was executed immediately.
+ *
+ * @private
+ */
+ whenVisible: function(fn, args) {
+ var me = this,
+ listener, pending, visible;
+ args = args || Ext.emptyArray;
+ listener = me.visibleListener;
+ pending = me.pendingVisible;
+ visible = me.isVisible(true);
+ if (!visible && !listener) {
+ me.visibleListener = Ext.on({
+ scope: me,
+ show: 'handleGlobalShow',
+ destroyable: true
+ });
+ }
+ if (visible) {
+ // Due to animations, it's possible that we may get called
+ // and the show event hasn't fired. If that is the case
+ // then just run now
+ if (pending) {
+ pending[fn] = args;
+ me.runWhenVisible();
+ } else {
+ me[fn].apply(me, args);
+ }
+ } else {
+ if (!pending) {
+ me.pendingVisible = pending = {};
+ }
+ pending[fn] = args;
+ }
+ return visible;
+ },
+ /**
+ * This method has the same arguments as {@link Ext.dom.Element#setXY element's setXY}
+ * method, but is used to maintain the `x` and `y` configs (for `floated` components)
+ * or the `left` and `top` config for positioned components.
+ * @param x
+ * @param y
+ * @param animation
+ * @private
+ * @since 6.5.0
+ */
+ setXY: function(x, y, animation) {
+ var me = this,
+ floated = me.getFloated();
+ if (typeof x !== 'number') {
+ animation = y;
+ y = x[1];
+ x = x[0];
+ }
+ me.$updatingXY = true;
+ if (floated) {
+ if (x != null) {
+ me.setX(x);
+ }
+ if (y != null) {
+ me.setY(y);
+ }
+ } else {
+ if (x != null) {
+ me.setLeft(x);
+ }
+ if (y != null) {
+ me.setTop(y);
+ }
+ }
+ me.$updatingXY = false;
+ if (floated) {
+ me.syncXYPosition(animation);
+ } else {
+ me.translate(x, y, animation);
+ }
+ },
+ syncXYPosition: function(animation) {
+ var me = this;
+ // Any positioning which is *not* within an align operation results in breaking the
+ // link between viewport and position.
+ if (!me.aligning) {
+ me.viewportResizeListener = Ext.destroy(me.viewportResizeListener);
+ }
+ me.callParent([
+ animation
+ ]);
+ }
+ },
+ deprecated: {
+ "6.2.0": {
+ methods: {
+ /**
+ * @method resetFloating
+ * @inheritdoc Ext.Component#resetPositioned
+ * @deprecated 6.2 Use {@link #resetPositioned} instead.
+ */
+ resetFloating: 'resetPositioned'
+ }
+ },
+ '6.5': {
+ configs: {
+ styleHtmlCls: null,
+ styleHtmlContent: null
+ }
+ }
+ }
+}, function(Cls) {
+ if (!document.querySelector('meta[name=viewport]')) {
+ Ext.log.warn('Ext JS requires a viewport meta tag in order to function correctly on mobile ' + 'devices. Please add the following tag to the of your html page: \n ' + ' ');
+ }
+});
+
+Ext.define('Ext.theme.material.Widget', {
+ override: 'Ext.Widget',
+ statics: {
+ floatInset: 16 / (window.devicePixelRatio || 1)
+ }
+});
+
+/**
+ * @private
+ */
+Ext.define('Ext.fx.State', {
+ isAnimatable: {
+ 'background-color': true,
+ 'background-image': true,
+ 'background-position': true,
+ 'border-bottom-color': true,
+ 'border-bottom-width': true,
+ 'border-color': true,
+ 'border-left-color': true,
+ 'border-left-width': true,
+ 'border-right-color': true,
+ 'border-right-width': true,
+ 'border-spacing': true,
+ 'border-top-color': true,
+ 'border-top-width': true,
+ 'border-width': true,
+ 'bottom': true,
+ 'color': true,
+ 'crop': true,
+ 'font-size': true,
+ 'font-weight': true,
+ 'height': true,
+ 'left': true,
+ 'letter-spacing': true,
+ 'line-height': true,
+ 'margin-bottom': true,
+ 'margin-left': true,
+ 'margin-right': true,
+ 'margin-top': true,
+ 'max-height': true,
+ 'max-width': true,
+ 'min-height': true,
+ 'min-width': true,
+ 'opacity': true,
+ 'outline-color': true,
+ 'outline-offset': true,
+ 'outline-width': true,
+ 'padding-bottom': true,
+ 'padding-left': true,
+ 'padding-right': true,
+ 'padding-top': true,
+ 'right': true,
+ 'text-indent': true,
+ 'text-shadow': true,
+ 'top': true,
+ 'vertical-align': true,
+ 'visibility': true,
+ 'width': true,
+ 'word-spacing': true,
+ 'z-index': true,
+ 'zoom': true,
+ 'transform': true
+ },
+ constructor: function(data) {
+ this.data = {};
+ this.set(data);
+ },
+ setConfig: function(data) {
+ this.set(data);
+ return this;
+ },
+ setRaw: function(data) {
+ this.data = data;
+ return this;
+ },
+ clear: function() {
+ return this.setRaw({});
+ },
+ setTransform: function(name, value) {
+ var data = this.data,
+ isArray = Ext.isArray(value),
+ transform = data.transform,
+ ln, key;
+ if (!transform) {
+ transform = data.transform = {
+ translateX: 0,
+ translateY: 0,
+ translateZ: 0,
+ scaleX: 1,
+ scaleY: 1,
+ scaleZ: 1,
+ rotate: 0,
+ rotateX: 0,
+ rotateY: 0,
+ rotateZ: 0,
+ skewX: 0,
+ skewY: 0
+ };
+ }
+ if (typeof name === 'string') {
+ switch (name) {
+ case 'translate':
+ if (isArray) {
+ ln = value.length;
+ if (ln === 0) {
+ break;
+ }
+ transform.translateX = value[0];
+ if (ln === 1) {
+ break;
+ }
+ transform.translateY = value[1];
+ if (ln === 2) {
+ break;
+ }
+ transform.translateZ = value[2];
+ } else {
+ transform.translateX = value;
+ };
+ break;
+ case 'rotate':
+ if (isArray) {
+ ln = value.length;
+ if (ln === 0) {
+ break;
+ }
+ transform.rotateX = value[0];
+ if (ln === 1) {
+ break;
+ }
+ transform.rotateY = value[1];
+ if (ln === 2) {
+ break;
+ }
+ transform.rotateZ = value[2];
+ } else {
+ transform.rotate = value;
+ };
+ break;
+ case 'scale':
+ if (isArray) {
+ ln = value.length;
+ if (ln === 0) {
+ break;
+ }
+ transform.scaleX = value[0];
+ if (ln === 1) {
+ break;
+ }
+ transform.scaleY = value[1];
+ if (ln === 2) {
+ break;
+ }
+ transform.scaleZ = value[2];
+ } else {
+ transform.scaleX = value;
+ transform.scaleY = value;
+ };
+ break;
+ case 'skew':
+ if (isArray) {
+ ln = value.length;
+ if (ln === 0) {
+ break;
+ }
+ transform.skewX = value[0];
+ if (ln === 1) {
+ break;
+ }
+ transform.skewY = value[1];
+ } else {
+ transform.skewX = value;
+ };
+ break;
+ default:
+ transform[name] = value;
+ }
+ } else {
+ for (key in name) {
+ if (name.hasOwnProperty(key)) {
+ value = name[key];
+ this.setTransform(key, value);
+ }
+ }
+ }
+ },
+ set: function(name, value) {
+ var data = this.data,
+ key;
+ if (typeof name !== 'string') {
+ for (key in name) {
+ value = name[key];
+ if (key === 'transform') {
+ this.setTransform(value);
+ } else {
+ data[key] = value;
+ }
+ }
+ } else {
+ if (name === 'transform') {
+ this.setTransform(value);
+ } else {
+ data[name] = value;
+ }
+ }
+ return this;
+ },
+ unset: function(name) {
+ var data = this.data;
+ if (data.hasOwnProperty(name)) {
+ delete data[name];
+ }
+ return this;
+ },
+ getData: function() {
+ return this.data;
+ }
+});
+
+/**
+ * @private
+ */
+Ext.define('Ext.fx.animation.Abstract', {
+ extend: Ext.Evented,
+ mixins: [
+ Ext.mixin.Factoryable
+ ],
+ factoryConfig: {
+ type: 'animation'
+ },
+ isAnimation: true,
+ config: {
+ name: '',
+ element: null,
+ /**
+ * @cfg {Object} before
+ * Before configuration.
+ */
+ before: null,
+ from: {},
+ to: {},
+ after: null,
+ states: {},
+ duration: 300,
+ /**
+ * @cfg {String} easing
+ * Easing type.
+ */
+ easing: 'linear',
+ iteration: 1,
+ direction: 'normal',
+ delay: 0,
+ onBeforeStart: null,
+ callback: null,
+ onEnd: null,
+ onBeforeEnd: null,
+ scope: null,
+ reverse: null,
+ preserveEndState: false,
+ replacePrevious: true
+ },
+ STATE_FROM: '0%',
+ STATE_TO: '100%',
+ DIRECTION_UP: 'up',
+ DIRECTION_TOP: 'top',
+ DIRECTION_DOWN: 'down',
+ DIRECTION_BOTTOM: 'bottom',
+ DIRECTION_LEFT: 'left',
+ DIRECTION_RIGHT: 'right',
+ stateNameRegex: /^(?:[\d.]+)%$/,
+ constructor: function() {
+ this.states = {};
+ this.callParent(arguments);
+ return this;
+ },
+ applyElement: function(element) {
+ return Ext.get(element);
+ },
+ applyBefore: function(before, current) {
+ if (before) {
+ return Ext.factory(before, Ext.fx.State, current);
+ }
+ },
+ applyAfter: function(after, current) {
+ if (after) {
+ return Ext.factory(after, Ext.fx.State, current);
+ }
+ },
+ setFrom: function(from) {
+ return this.setState(this.STATE_FROM, from);
+ },
+ setTo: function(to) {
+ return this.setState(this.STATE_TO, to);
+ },
+ getFrom: function() {
+ return this.getState(this.STATE_FROM);
+ },
+ getTo: function() {
+ return this.getState(this.STATE_TO);
+ },
+ setStates: function(states) {
+ var validNameRegex = this.stateNameRegex,
+ name;
+ for (name in states) {
+ if (validNameRegex.test(name)) {
+ this.setState(name, states[name]);
+ }
+ }
+ return this;
+ },
+ getStates: function() {
+ return this.states;
+ },
+ updateCallback: function(callback) {
+ if (callback) {
+ this.setOnEnd(callback);
+ }
+ },
+ end: function() {
+ // alias for stop so that the following api is the same between ext/touch:
+ // element.getActiveAnimation().end()
+ this.stop();
+ },
+ stop: function() {
+ this.fireEvent('stop', this);
+ },
+ destroy: function() {
+ // Event handlers need to know this.
+ this.destroying = true;
+ this.stop();
+ this.callParent();
+ this.destroying = false;
+ this.destroyed = true;
+ },
+ setState: function(name, state) {
+ var states = this.getStates(),
+ stateInstance;
+ stateInstance = Ext.factory(state, Ext.fx.State, states[name]);
+ if (stateInstance) {
+ states[name] = stateInstance;
+ } else if (name === this.STATE_TO) {
+ Ext.Logger.error("Setting and invalid '100%' / 'to' state of: " + state);
+ }
+ return this;
+ },
+ getState: function(name) {
+ return this.getStates()[name];
+ },
+ getData: function() {
+ var me = this,
+ states = me.getStates(),
+ statesData = {},
+ before = me.getBefore(),
+ after = me.getAfter(),
+ from = states[me.STATE_FROM],
+ to = states[me.STATE_TO],
+ fromData = from.getData(),
+ toData = to.getData(),
+ data, name, state;
+ for (name in states) {
+ if (states.hasOwnProperty(name)) {
+ state = states[name];
+ data = state.getData();
+ statesData[name] = data;
+ }
+ }
+ return {
+ before: before ? before.getData() : {},
+ after: after ? after.getData() : {},
+ states: statesData,
+ from: fromData,
+ to: toData,
+ duration: me.getDuration(),
+ iteration: me.getIteration(),
+ direction: me.getDirection(),
+ easing: me.getEasing(),
+ delay: me.getDelay(),
+ onEnd: me.getOnEnd(),
+ onBeforeEnd: me.getOnBeforeEnd(),
+ onBeforeStart: me.getOnBeforeStart(),
+ scope: me.getScope(),
+ preserveEndState: me.getPreserveEndState(),
+ replacePrevious: me.getReplacePrevious()
+ };
+ }
+});
+
+/**
+ * @private
+ */
+Ext.define('Ext.fx.animation.Slide', {
+ extend: Ext.fx.animation.Abstract,
+ alternateClassName: 'Ext.fx.animation.SlideIn',
+ alias: [
+ 'animation.slide',
+ 'animation.slideIn'
+ ],
+ config: {
+ /**
+ * @cfg {String} direction
+ * The direction of which the slide animates
+ * @accessor
+ */
+ direction: 'left',
+ /**
+ * @cfg {Boolean} out
+ * True if you want to make this animation slide out, instead of slide in.
+ * @accessor
+ */
+ out: false,
+ /**
+ * @cfg {Number} offset
+ * The offset that the animation should go offscreen before entering (or when exiting)
+ * @accessor
+ */
+ offset: 0,
+ /**
+ * @cfg easing
+ * @inheritdoc
+ */
+ easing: 'auto',
+ containerBox: 'auto',
+ elementBox: 'auto',
+ isElementBoxFit: true,
+ useCssTransform: true
+ },
+ reverseDirectionMap: {
+ up: 'down',
+ top: 'down',
+ down: 'up',
+ bottom: 'up',
+ left: 'right',
+ right: 'left'
+ },
+ applyEasing: function(easing) {
+ if (easing === 'auto') {
+ return 'ease-' + ((this.getOut()) ? 'in' : 'out');
+ }
+ return easing;
+ },
+ getContainerBox: function() {
+ var box = this._containerBox;
+ if (box === 'auto') {
+ box = this.getElement().getParent().getBox();
+ }
+ return box;
+ },
+ getElementBox: function() {
+ var box = this._elementBox;
+ if (this.getIsElementBoxFit()) {
+ return this.getContainerBox();
+ }
+ if (box === 'auto') {
+ box = this.getElement().getBox();
+ }
+ return box;
+ },
+ getData: function() {
+ var elementBox = this.getElementBox(),
+ containerBox = this.getContainerBox(),
+ box = elementBox ? elementBox : containerBox,
+ from = this.getFrom(),
+ to = this.getTo(),
+ out = this.getOut(),
+ offset = this.getOffset(),
+ direction = this.getDirection(),
+ useCssTransform = this.getUseCssTransform(),
+ reverse = this.getReverse(),
+ translateX = 0,
+ translateY = 0,
+ offsetPct, fromX, fromY, toX, toY;
+ if (typeof offset === 'string') {
+ offsetPct = true;
+ offset = parseFloat(offset);
+ }
+ if (reverse) {
+ direction = this.reverseDirectionMap[direction];
+ }
+ switch (direction) {
+ case this.DIRECTION_UP:
+ case this.DIRECTION_TOP:
+ if (offsetPct) {
+ offset = box.height * offset / 100;
+ };
+ if (out) {
+ translateY = containerBox.top - box.top - box.height - offset;
+ } else {
+ translateY = containerBox.bottom - box.bottom + box.height + offset;
+ };
+ break;
+ case this.DIRECTION_DOWN:
+ case this.DIRECTION_BOTTOM:
+ if (offsetPct) {
+ offset = box.height * offset / 100;
+ };
+ if (out) {
+ translateY = containerBox.bottom - box.bottom + box.height + offset;
+ } else {
+ translateY = containerBox.top - box.height - box.top - offset;
+ };
+ break;
+ case this.DIRECTION_RIGHT:
+ if (offsetPct) {
+ offset = box.width * offset / 100;
+ };
+ if (out) {
+ translateX = containerBox.right - box.right + box.width + offset;
+ } else {
+ translateX = containerBox.left - box.left - box.width - offset;
+ };
+ break;
+ case this.DIRECTION_LEFT:
+ if (offsetPct) {
+ offset = box.width * offset / 100;
+ };
+ if (out) {
+ translateX = containerBox.left - box.left - box.width - offset;
+ } else {
+ translateX = containerBox.right - box.right + box.width + offset;
+ };
+ break;
+ }
+ fromX = (out) ? 0 : translateX;
+ fromY = (out) ? 0 : translateY;
+ if (useCssTransform) {
+ from.setTransform({
+ translateX: fromX,
+ translateY: fromY
+ });
+ } else {
+ from.set('left', fromX);
+ from.set('top', fromY);
+ }
+ toX = (out) ? translateX : 0;
+ toY = (out) ? translateY : 0;
+ if (useCssTransform) {
+ to.setTransform({
+ translateX: toX,
+ translateY: toY
+ });
+ } else {
+ to.set('left', toX);
+ to.set('top', toY);
+ }
+ return this.callParent(arguments);
+ }
+});
+
+/**
+ * @private
+ */
+Ext.define('Ext.fx.animation.SlideOut', {
+ extend: Ext.fx.animation.Slide,
+ alias: [
+ 'animation.slideOut'
+ ],
+ config: {
+ // @hide
+ out: true
+ }
+});
+
+/**
+ * @private
+ */
+Ext.define('Ext.fx.animation.Fade', {
+ extend: Ext.fx.animation.Abstract,
+ alternateClassName: 'Ext.fx.animation.FadeIn',
+ alias: [
+ 'animation.fade',
+ 'animation.fadeIn'
+ ],
+ config: {
+ /**
+ * @cfg {Boolean} out True if you want to make this animation fade out, instead of fade in.
+ * @accessor
+ */
+ out: false,
+ // Needs to default to false so updater runs and populates from/to data.
+ before: {
+ display: null,
+ opacity: 0
+ },
+ after: {
+ opacity: null
+ },
+ reverse: null
+ },
+ updateOut: function(newOut) {
+ var to = this.getTo(),
+ from = this.getFrom();
+ if (newOut) {
+ from.set('opacity', 1);
+ to.set('opacity', 0);
+ } else {
+ from.set('opacity', 0);
+ to.set('opacity', 1);
+ }
+ }
+});
+
+/**
+ * @private
+ */
+Ext.define('Ext.fx.animation.FadeOut', {
+ extend: Ext.fx.animation.Fade,
+ alias: 'animation.fadeOut',
+ config: {
+ // @hide
+ out: true,
+ before: {}
+ }
+});
+
+/**
+ * @private
+ */
+Ext.define('Ext.fx.animation.Flip', {
+ extend: Ext.fx.animation.Abstract,
+ alias: 'animation.flip',
+ config: {
+ easing: 'ease-in',
+ /**
+ * @cfg {String} direction The direction of which the slide animates
+ * @accessor
+ */
+ direction: 'right',
+ half: false,
+ out: null
+ },
+ getData: function() {
+ var me = this,
+ from = me.getFrom(),
+ to = me.getTo(),
+ direction = me.getDirection(),
+ out = me.getOut(),
+ half = me.getHalf(),
+ rotate = half ? 90 : 180,
+ fromScale = 1,
+ toScale = 1,
+ fromRotateX = 0,
+ fromRotateY = 0,
+ toRotateX = 0,
+ toRotateY = 0;
+ if (out) {
+ toScale = 0.8;
+ } else {
+ fromScale = 0.8;
+ }
+ switch (direction) {
+ case this.DIRECTION_UP:
+ case this.DIRECTION_TOP:
+ if (out) {
+ toRotateX = rotate;
+ } else {
+ fromRotateX = -rotate;
+ };
+ break;
+ case this.DIRECTION_DOWN:
+ case this.DIRECTION_BOTTOM:
+ if (out) {
+ toRotateX = -rotate;
+ } else {
+ fromRotateX = rotate;
+ };
+ break;
+ case this.DIRECTION_RIGHT:
+ if (out) {
+ toRotateY = rotate;
+ } else {
+ fromRotateY = -rotate;
+ };
+ break;
+ case this.DIRECTION_LEFT:
+ if (out) {
+ toRotateY = -rotate;
+ } else {
+ fromRotateY = rotate;
+ };
+ break;
+ }
+ from.setTransform({
+ rotateX: fromRotateX,
+ rotateY: fromRotateY,
+ scale: fromScale
+ });
+ to.setTransform({
+ rotateX: toRotateX,
+ rotateY: toRotateY,
+ scale: toScale
+ });
+ return this.callParent();
+ }
+});
+
+/**
+ * @private
+ */
+Ext.define('Ext.fx.animation.Pop', {
+ extend: Ext.fx.animation.Abstract,
+ alias: [
+ 'animation.pop',
+ 'animation.popIn'
+ ],
+ alternateClassName: 'Ext.fx.animation.PopIn',
+ config: {
+ /**
+ * @cfg {Boolean} out True if you want to make this animation pop out, instead of pop in.
+ * @accessor
+ */
+ out: false,
+ before: {
+ display: null,
+ opacity: 0
+ },
+ after: {
+ opacity: null
+ }
+ },
+ getData: function() {
+ var to = this.getTo(),
+ from = this.getFrom(),
+ out = this.getOut();
+ if (out) {
+ from.set('opacity', 1);
+ from.setTransform({
+ scale: 1
+ });
+ to.set('opacity', 0);
+ to.setTransform({
+ scale: 0
+ });
+ } else {
+ from.set('opacity', 0);
+ from.setTransform({
+ scale: 0
+ });
+ to.set('opacity', 1);
+ to.setTransform({
+ scale: 1
+ });
+ }
+ return this.callParent(arguments);
+ }
+});
+
+/**
+ * @private
+ */
+Ext.define('Ext.fx.animation.PopOut', {
+ extend: Ext.fx.animation.Pop,
+ alias: 'animation.popOut',
+ config: {
+ // @hide
+ out: true,
+ before: {}
+ }
+});
+
+/**
+ * @private
+ * This class is a factory class that will create and return an animation class based on the
+ * {@link #type} configuration.
+ */
+Ext.define('Ext.fx.Animation', {
+ /**
+ * @cfg {String} type The type of animation to use. The possible values are:
+ *
+ * - `fade` - {@link Ext.fx.animation.Fade}
+ * - `fadeOut` - {@link Ext.fx.animation.FadeOut}
+ * - `flip` - {@link Ext.fx.animation.Flip}
+ * - `pop` - {@link Ext.fx.animation.Pop}
+ * - `popOut` - {@link Ext.fx.animation.PopOut}
+ * - `slide` - {@link Ext.fx.animation.Slide}
+ * - `slideOut` - {@link Ext.fx.animation.SlideOut}
+ */
+ constructor: function(config) {
+ var defaultClass = Ext.fx.animation.Abstract,
+ type;
+ if (typeof config === 'string') {
+ type = config;
+ config = {};
+ } else if (config && config.type) {
+ type = config.type;
+ }
+ if (type) {
+ defaultClass = Ext.ClassManager.getByAlias('animation.' + type);
+ if (!defaultClass) {
+ Ext.Logger.error("Invalid animation type of: '" + type + "'");
+ }
+ }
+ return Ext.factory(config, defaultClass);
+ }
+});
+
+/**
+ * @class Ext.Progress
+ *
+ * @example
+ * Ext.create({
+ * xtype: 'grid',
+ * title: 'Simpsons',
+ * store: {
+ * data: [
+ * { name: 'Lisa', progress: .159 },
+ * { name: 'Bart', progress: .216 },
+ * { name: 'Homer', progress: .55 },
+ * { name: 'Maggie', progress: .167 },
+ * { name: 'Marge', progress: .145 }
+ * ]
+ * },
+ * columns: [
+ * { text: 'Name', dataIndex: 'name' },
+ * {
+ * text: 'Progress',
+ * width: 120,
+ * dataIndex: 'progress',
+ * cell: {
+ * xtype: 'widgetcell',
+ * widget: {
+ * xtype: 'progress'
+ * }
+ * }
+ * }
+ * ],
+ * height: 200,
+ * width: 400,
+ * fullscreen: true
+ * });
+ */
+Ext.define('Ext.overrides.Progress', {
+ override: 'Ext.Progress',
+ initialize: function() {
+ this.callParent();
+ this.on('painted', 'onPainted', this);
+ },
+ onPainted: function() {
+ this.syncWidth();
+ },
+ onResize: function(width) {
+ this.syncWidth(width);
+ },
+ syncWidth: function(width) {
+ var me = this;
+ if (width == null) {
+ width = me.element.getWidth();
+ }
+ me.backgroundEl.setWidth(width);
+ me.textEl.setWidth(width);
+ },
+ privates: {
+ startBarAnimation: function(o) {
+ var me = this;
+ me.barAnim = new Ext.fx.Animation(Ext.apply(o, {
+ element: me.barEl,
+ preserveEndState: true,
+ callback: function() {
+ delete me.barAnim;
+ }
+ }));
+ Ext.Animator.run(me.barAnim);
+ },
+ stopBarAnimation: function() {
+ var barAnim = this.barAnim;
+ if (barAnim) {
+ barAnim.destroy();
+ }
+ this.barAnim = null;
+ }
+ }
+});
+
+/**
+ * This class is a base class for an event domain. In the context of MVC, an "event domain"
+ * is one or more base classes that fire events to which a Controller wants to listen. A
+ * controller listens to events by describing the selectors for events of interest to it.
+ *
+ * Matching selectors to the firer of an event is one key aspect that defines an event
+ * domain. All event domain instances must provide a `match` method that tests selectors
+ * against the event firer.
+ *
+ * When an event domain instance is created (typically as a `singleton`), its `type`
+ * property is used to catalog the domain in the
+ * {@link Ext.app.EventDomain#instances Ext.app.EventDomain.instances} map.
+ *
+ * There are five event domains provided by default:
+ *
+ * - {@link Ext.app.domain.Component Component domain}. This is the primary event domain that
+ * has been available since Ext JS MVC was introduced. This domain is defined as any class that
+ * extends {@link Ext.Component}, where the selectors use
+ * {@link Ext.ComponentQuery#query Ext.ComponentQuery}.
+ * - {@link Ext.app.domain.Global Global domain}. This domain provides Controllers with access
+ * to events fired from {@link Ext.GlobalEvents} Observable instance. These events represent
+ * the state of the application as a whole, and are always anonymous. Because of this, Global
+ * domain does not provide selectors at all.
+ * - {@link Ext.app.domain.Controller Controller domain}. This domain includes all classes
+ * that extend {@link Ext.app.Controller}. Events fired by Controllers will be available
+ * within this domain; selectors are either Controller's {@link Ext.app.Controller#id id} or
+ * '*' wildcard for any Controller.
+ * - {@link Ext.app.domain.Store Store domain}. This domain is for classes extending
+ * {@link Ext.data.AbstractStore}. Selectors are either Store's
+ * {@link Ext.data.AbstractStore#storeId storeId} or '*' wildcard for any Store.
+ * - {@link Ext.app.domain.Direct Direct domain}. This domain includes all classes that extend
+ * {@link Ext.direct.Provider}. Selectors are either Provider's {@link Ext.direct.Provider#id id}
+ * or '*' wildcard for any Provider. This domain is optional and will be loaded only if
+ * {@link Ext.direct.Manager} singleton is required in your application.
+ */
+Ext.define('Ext.app.EventDomain', {
+ statics: {
+ /**
+ * An object map containing `Ext.app.EventDomain` instances keyed by the value
+ * of their `type` property.
+ */
+ instances: {}
+ },
+ /**
+ * @cfg {String} idProperty Name of the identifier property for this event domain.
+ */
+ isEventDomain: true,
+ isInstance: false,
+ constructor: function() {
+ var me = this;
+ if (!me.isInstance) {
+ Ext.app.EventDomain.instances[me.type] = me;
+ }
+ me.bus = {};
+ me.monitoredClasses = [];
+ },
+ /**
+ * This method dispatches an event fired by an object monitored by this domain. This
+ * is not called directly but is called by interceptors injected by the `monitor` method.
+ *
+ * @param {Object} target The firer of the event.
+ * @param {String} ev The event being fired.
+ * @param {Array} args The arguments for the event. This array **does not** include the
+ * event name. That has already been sliced off because this class intercepts the
+ * {@link Ext.util.Observable#fireEventArgs fireEventArgs} method which takes an array
+ * as the event's argument list.
+ *
+ * @return {Boolean} `false` if any listener returned `false`, otherwise `true`.
+ *
+ * @private
+ */
+ dispatch: function(target, ev, args) {
+ ev = Ext.canonicalEventName(ev);
+ /* eslint-disable-next-line vars-on-top */
+ var me = this,
+ bus = me.bus,
+ selectors = bus[ev],
+ selector, controllers, id, info, events, len, i, event;
+ if (!selectors) {
+ return true;
+ }
+ // Loop over all the selectors that are bound to this event
+ for (selector in selectors) {
+ // Check if the target matches the selector, note that we will only have
+ // me.controller when we're an instance of a domain.View attached to a view controller.
+ if (selectors.hasOwnProperty(selector) && me.match(target, selector, me.controller)) {
+ // Loop over all the controllers that are bound to this selector
+ controllers = selectors[selector];
+ for (id in controllers) {
+ if (controllers.hasOwnProperty(id)) {
+ info = controllers[id];
+ if (info.controller.isActive()) {
+ // Loop over all the events that are bound to this selector
+ // on the current controller
+ events = info.list;
+ len = events.length;
+ for (i = 0; i < len; i++) {
+ event = events[i];
+ // Fire the event!
+ if (event.fire.apply(event, args) === false) {
+ return false;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ return true;
+ },
+ /**
+ * This method adds listeners on behalf of a controller. This method is passed an
+ * object that is keyed by selectors. The value of these is also an object but now
+ * keyed by event name. For example:
+ *
+ * domain.listen({
+ * 'some[selector]': {
+ * click: function() { ... }
+ * },
+ *
+ * 'other selector': {
+ * change: {
+ * fn: function() { ... },
+ * delay: 10
+ * }
+ * }
+ *
+ * }, controller);
+ *
+ * @param {Object} selectors Config object containing selectors and listeners.
+ * @param {Ext.app.BaseController} [controller] (private)
+ * @private
+ */
+ listen: function(selectors, controller) {
+ var me = this,
+ bus = me.bus,
+ idProperty = me.idProperty,
+ monitoredClasses = me.monitoredClasses,
+ monitoredClassesCount = monitoredClasses.length,
+ controllerId = controller.getId(),
+ isComponentDomain = (me.type === 'component'),
+ refMap = isComponentDomain ? controller.getRefMap() : null,
+ i, tree, info, selector, options, listener, scope, event, listeners, ev, classHasListeners;
+ for (selector in selectors) {
+ listeners = selectors[selector];
+ if (isComponentDomain) {
+ // This allows ref names to be used as selectors, e.g.
+ // refs: {
+ // nav: '#navigationList
+ // },
+ // control: {
+ // nav: {
+ // itemclick: 'onNavClick'
+ // }
+ // }
+ //
+ // We process this here instead of in the controller so that we don't
+ // have to do multiple loops over the selectors
+ selector = refMap[selector] || selector;
+ }
+ if (listeners) {
+ if (idProperty) {
+ if (!/^[*#]/.test(selector)) {
+ Ext.raise('Selectors containing id should begin with #');
+ }
+ selector = selector === '*' ? selector : selector.substring(1);
+ }
+ for (ev in listeners) {
+ options = null;
+ listener = listeners[ev];
+ scope = controller;
+ ev = Ext.canonicalEventName(ev);
+ event = new Ext.util.Event(controller, ev);
+ // Normalize the listener
+ if (Ext.isObject(listener)) {
+ options = listener;
+ listener = options.fn;
+ scope = options.scope || controller;
+ delete options.fn;
+ delete options.scope;
+ }
+ if ((!options || !options.scope) && typeof listener === 'string') {
+ // Allow this lookup to be dynamic in debug mode.
+ // Super useful for testing!
+ if (!scope[listener]) {
+ Ext.raise('Cannot resolve "' + listener + '" on controller.');
+ }
+ scope = null;
+ } else if (typeof listener === 'string') {
+ listener = scope[listener];
+ }
+ event.addListener(listener, scope, options);
+ for (i = 0; i < monitoredClassesCount; ++i) {
+ classHasListeners = monitoredClasses[i].hasListeners;
+ if (classHasListeners) {
+ // Ext.mixin.Observable doesn't have hasListeners at class level
+ classHasListeners._incr_(ev);
+ }
+ }
+ // Create the bus tree if it is not there yet
+ tree = bus[ev] || (bus[ev] = {});
+ tree = tree[selector] || (tree[selector] = {});
+ info = tree[controllerId] || (tree[controllerId] = {
+ controller: controller,
+ list: []
+ });
+ // Push our listener in our bus
+ info.list.push(event);
+ }
+ }
+ }
+ },
+ /**
+ * This method matches the firer of the event (the `target`) to the given `selector`.
+ * Default matching is very simple: a match is true when selector equals target's
+ * {@link #cfg-idProperty idProperty}, or when selector is '*' wildcard to match any
+ * target.
+ *
+ * @param {Object} target The firer of the event.
+ * @param {String} selector The selector to which to match the `target`.
+ *
+ * @return {Boolean} `true` if the `target` matches the `selector`.
+ *
+ * @protected
+ */
+ match: function(target, selector) {
+ var idProperty = this.idProperty;
+ if (idProperty) {
+ return selector === '*' || target[idProperty] === selector;
+ }
+ return false;
+ },
+ /**
+ * This method is called by the derived class to monitor `fireEvent` calls. Any call
+ * to `fireEvent` on the target Observable will be intercepted and dispatched to any
+ * listening Controllers. Assuming the original `fireEvent` method does not return
+ * `false`, the event is passed to the `dispatch` method of this object.
+ *
+ * This is typically called in the `constructor` of derived classes.
+ *
+ * @param {Ext.Class} observable The Observable to monitor for events.
+ *
+ * @protected
+ */
+ monitor: function(observable) {
+ var domain = this,
+ prototype = observable.isInstance ? observable : observable.prototype,
+ doFireEvent = prototype.doFireEvent;
+ domain.monitoredClasses.push(observable);
+ prototype.doFireEvent = function(ev, args) {
+ var me = this,
+ ret;
+ ret = doFireEvent.apply(me, arguments);
+ // Observable can be destroyed in the event handler above,
+ // in which case we can't proceed with dispatching domain event.
+ if (ret !== false && !me.destroyed && !me.isSuspended(ev)) {
+ ret = domain.dispatch(me, ev, args);
+ }
+ return ret;
+ };
+ },
+ /**
+ * Removes all of a controller's attached listeners.
+ *
+ * @param {String} controllerId The id of the controller.
+ *
+ * @private
+ */
+ unlisten: function(controllerId) {
+ var bus = this.bus,
+ id = controllerId,
+ monitoredClasses = this.monitoredClasses,
+ monitoredClassesCount = monitoredClasses.length,
+ controllers, ev, events, len, item, selector, selectors, i, j, info, classHasListeners;
+ if (controllerId.isController) {
+ id = controllerId.getId();
+ }
+ for (ev in bus) {
+ ev = Ext.canonicalEventName(ev);
+ if (bus.hasOwnProperty(ev) && (selectors = bus[ev])) {
+ for (selector in selectors) {
+ controllers = selectors[selector];
+ info = controllers[id];
+ if (info) {
+ events = info.list;
+ if (events) {
+ for (i = 0 , len = events.length; i < len; ++i) {
+ item = events[i];
+ item.clearListeners();
+ for (j = 0; j < monitoredClassesCount; ++j) {
+ classHasListeners = monitoredClasses[j].hasListeners;
+ if (classHasListeners) {
+ // Ext.mixin.Observable doesn't have hasListeners
+ // at class level
+ classHasListeners._decr_(item.name);
+ }
+ }
+ }
+ delete controllers[id];
+ }
+ }
+ }
+ }
+ }
+ },
+ destroy: function() {
+ this.monitoredClasses = this.bus = null;
+ this.callParent();
+ }
+});
+
+/**
+ * This class implements the component event domain. All classes extending from
+ * {@link Ext.Component} are included in this domain. The matching criteria uses
+ * {@link Ext.ComponentQuery}.
+ *
+ * @private
+ */
+Ext.define('Ext.app.domain.Component', {
+ extend: Ext.app.EventDomain,
+ singleton: true,
+ type: 'component',
+ constructor: function() {
+ this.callParent();
+ this.monitor(Ext.Widget);
+ },
+ dispatch: function(target, ev, args) {
+ var controller = target.lookupController(false),
+ // don't skip target
+ domain, view;
+ while (controller) {
+ domain = controller.compDomain;
+ if (domain) {
+ if (domain.dispatch(target, ev, args) === false) {
+ return false;
+ }
+ }
+ view = controller.getView();
+ controller = view ? view.lookupController(true) : null;
+ }
+ return this.callParent([
+ target,
+ ev,
+ args
+ ]);
+ },
+ match: function(target, selector) {
+ return target.is(selector);
+ }
+});
+
+/**
+ * This class manages event dispatching for Controllers. The details of connecting classes
+ * to this dispatching mechanism is delegated to {@link Ext.app.EventDomain} instances.
+ *
+ * @private
+ */
+Ext.define('Ext.app.EventBus', {
+ singleton: true,
+ constructor: function() {
+ var me = this,
+ domains = Ext.app.EventDomain.instances;
+ me.callParent();
+ me.domains = domains;
+ me.bus = domains.component.bus;
+ },
+ // compat
+ /**
+ * Adds a set of component event listeners for a controller. To work with event domains
+ * other than component, see {@link #listen}.
+ *
+ * @param {Object} selectors Config object containing selectors and listeners.
+ * @param {Ext.app.BaseController} controller The listening controller instance.
+ */
+ control: function(selectors, controller) {
+ return this.domains.component.listen(selectors, controller);
+ },
+ /**
+ * Adds a set of event domain listeners for a controller. For more information on event
+ * domains, see {@link Ext.app.EventDomain} and {@link Ext.app.BaseController}.
+ *
+ * @param {Object} to Config object containing domains, selectors and listeners.
+ * @param {Ext.app.BaseController} controller The listening controller instance.
+ */
+ listen: function(to, controller) {
+ var domains = this.domains,
+ domain;
+ for (domain in to) {
+ if (to.hasOwnProperty(domain)) {
+ domains[domain].listen(to[domain], controller);
+ }
+ }
+ },
+ /**
+ * Removes all of a controller's attached listeners.
+ *
+ * @param {String/Ext.app.BaseController} controllerId The id or the controller instance.
+ */
+ unlisten: function(controllerId) {
+ var domains = Ext.app.EventDomain.instances,
+ domain;
+ for (domain in domains) {
+ domains[domain].unlisten(controllerId);
+ }
+ }
+});
+
+/**
+ * This class implements the global event domain. This domain represents event fired from
+ * {@link Ext.GlobalEvents} Observable instance. No selectors are supported for this domain.
+ *
+ * @private
+ */
+Ext.define('Ext.app.domain.Global', {
+ extend: Ext.app.EventDomain,
+ singleton: true,
+ type: 'global',
+ constructor: function() {
+ var me = this;
+ me.callParent();
+ me.monitor(Ext.GlobalEvents);
+ },
+ /**
+ * This method adds listeners on behalf of a controller. Since Global domain does not
+ * support selectors, we skip this layer and just accept an object keyed by events.
+ * For example:
+ *
+ * domain.listen({
+ * idle: function() { ... },
+ * afterlayout: {
+ * fn: function() { ... },
+ * delay: 10
+ * }
+ * });
+ *
+ * @param {Object} listeners Config object containing listeners.
+ * @param {Object} controller A controller to force execution scope on
+ *
+ * @private
+ */
+ listen: function(listeners, controller) {
+ this.callParent([
+ {
+ global: listeners
+ },
+ controller
+ ]);
+ },
+ match: Ext.returnTrue
+});
+
+/**
+ * This class is used to hold the handler functions for when a route is executed. It also
+ * keeps track of the {@link #lastToken last token} this handler was last executed on.
+ *
+ * @since 6.6.0
+ */
+Ext.define('Ext.route.Handler', {
+ /**
+ * @cfg {Function/String} action
+ * The handler to execute when the route recognizes a token in the hash.
+ *
+ * This can be prevented from executing by the {@link #before} handler.
+ *
+ * If defined as a String, the Function will be resolved from the {@link #scope}.
+ */
+ /**
+ * @cfg {Function/String} before
+ * The handler to execute before the {@link #action} handler. The `before` handler
+ * can prevent the {@link #action} handler by executing the {@link Ext.route.Action#stop stop}
+ * method on the {@link Ext.route.Action action} argument or by returning a
+ * {@link Ext.Promise promise} and rejecting it:
+ *
+ * routes: {
+ * 'user/:id': {
+ * before: 'onBefore
+ * }
+ * },
+ *
+ * onBefore: function (id, action) {
+ * action.stop();
+ * }
+ *
+ * // or
+ *
+ * onBefore: function (id) {
+ * return new Ext.Promise(function (resolve, reject) {
+ * reject();
+ * });
+ * }
+ *
+ * If using the `action` argument, the `action` argument will always be the last argument passed
+ * after any configured url parameters.
+ *
+ * If defined as a String, the Function will be resolved from the {@link #scope}.
+ */
+ /**
+ * @cfg {Boolean} lazy
+ * If `true`, the defined routes will get executed when created.
+ */
+ lazy: false,
+ /**
+ * @cfg {Function/String} exit
+ * The handler to execute when the route no longer recognizes a token in the current hash.
+ *
+ * If defined as a String, the Function will be resolved from the {@link #scope}.
+ */
+ /**
+ * @property {Ext.route.Route} route
+ * The route this handler is connected to.
+ */
+ /**
+ * @cfg {Ext.Base} scope
+ * The scope to call the handlers with. If the handlers are defined with a String,
+ * the handlers will be resolved from this scope.
+ */
+ /**
+ * @cfg {Boolean} single
+ * Controls if this handler should be removed after first execution. There are
+ * a veriety of values that control when in the execution this should be removed:
+ *
+ * - **true** Remove this handler after a successful and full execution. The handler
+ * will be removed after the handler's {@link #cfg!action} has been executed. If a
+ * {@link #cfg!before} has been rejected, the {@link #cfg!action} will not be
+ * executed meaning this handler will **not** be removed.
+ * - **after** Remove this handler after the {@cfg!link #before} has been resolved.
+ * If {@cfg!link #before} has been rejected, this handler will **not** be removed.
+ * If no {@cfg!link #before} exists, this handler will be removed prior to the
+ * {@link #cfg!action} being executed.
+ * - **before** Remove this handler before the {@link #cfg!before} has been
+ * executed. If no {@link #cfg!before} exists, this handler will be removed prior
+ * to the {@link #cfg!action} being executed.
+ */
+ /**
+ * @private
+ * @property {String} lastToken
+ * The last token this handler is connected to in the current hash.
+ */
+ statics: {
+ /**
+ * @private
+ *
+ * Creates a {@link Ext.route.Handler Handler} instance from the config
+ * defined in the {@link Ext.route.Mixin#routes} config.
+ *
+ * @param {Object} config The config from the routes config.
+ * @param {Ext.Base} scope The scope the handlers will be called/resolved with.
+ * @return {Ext.route.Handler}
+ */
+ fromRouteConfig: function(config, scope) {
+ var handler = {
+ action: config.action,
+ before: config.before,
+ lazy: config.lazy,
+ exit: config.exit,
+ scope: scope,
+ single: config.single
+ };
+ return new this(handler);
+ }
+ },
+ constructor: function(config) {
+ Ext.apply(this, config);
+ }
+});
+
+/**
+ * Class that can manage the execution of route handlers. All {@link #befores} handlers
+ * will be executed prior to the {@link #actions} handlers. If at any point this `Action`
+ * class is stopped, no other handler (before or action) will be executed.
+ */
+Ext.define('Ext.route.Action', {
+ config: {
+ /**
+ * @cfg {Function[]} actions
+ * The action handlers to execute in response to the route executing.
+ * The individual functions will be executed with the scope of the class
+ * that connected the route and the arguments will be the configured URL
+ * parameters in order they appear in the token.
+ *
+ * See {@link #befores} also.
+ */
+ actions: null,
+ /**
+ * @cfg {Function[]} befores
+ * The before handlers to execute prior to the {@link #actions} handlers.
+ * The individual functions will be executed with the scope of the class
+ * that connected the route and the arguments will be the configured URL
+ * parameters in the order they appear in the token plus this `Action` instance
+ * as the last argument.
+ *
+ * **IMPORTANT** A before function must have a resolution. You can do this
+ * by executing the {@link #resume} or {@link #stop} function or you can
+ * return a promise and resolve/reject it.
+ *
+ * var action = new Ext.route.Action({
+ * before: {
+ * fn: function (action) {
+ * action.resume(); //or action.stop();
+ * }
+ * }
+ * });
+ * action.run();
+ *
+ * var action = new Ext.route.Action({
+ * before: {
+ * fn: function () {
+ * return new Ext.Promise(function (resolve, reject) {
+ * resolve(); //or reject();
+ * });
+ * }
+ * }
+ * });
+ * action.run();
+ *
+ * See {@link #actions} also.
+ */
+ befores: null,
+ /**
+ * @cfg {Array} urlParams
+ * The URL parameters that were matched by the {@link Ext.route.Route}.
+ */
+ urlParams: []
+ },
+ /**
+ * @property {Ext.Deferred} deferred
+ * The deferral object that will resolve after all functions have executed
+ * ({@link #befores} and {@link #actions}) or reject if any {@link #befores}
+ * function stops this action.
+ * @private
+ */
+ /**
+ * @property {Boolean} [started=false]
+ * Whether or not this class has started executing any {@link #befores} or {@link #actions}.
+ * @readonly
+ * @protected
+ */
+ started: false,
+ /**
+ * @property {Boolean} [stopped=false]
+ * Whether or not this class was stopped by a {@link #befores} function.
+ * @readonly
+ * @protected
+ */
+ stopped: false,
+ constructor: function(config) {
+ var me = this;
+ me.deferred = new Ext.Deferred();
+ me.resume = me.resume.bind(me);
+ me.stop = me.stop.bind(me);
+ me.initConfig(config);
+ me.callParent([
+ config
+ ]);
+ },
+ applyActions: function(actions) {
+ if (actions) {
+ actions = Ext.Array.from(actions);
+ }
+ return actions;
+ },
+ applyBefores: function(befores) {
+ if (befores) {
+ befores = Ext.Array.from(befores);
+ }
+ return befores;
+ },
+ destroy: function() {
+ this.deferred = null;
+ this.setBefores(null).setActions(null).setUrlParams(null);
+ this.callParent();
+ },
+ /**
+ * Allow further function execution of other functions if any.
+ *
+ * @return {Ext.route.Action} this
+ */
+ resume: function() {
+ return this.next();
+ },
+ /**
+ * Prevent other functions from executing and resolve the {@link #deferred}.
+ *
+ * @return {Ext.route.Action} this
+ */
+ stop: function() {
+ this.stopped = true;
+ return this.done();
+ },
+ /**
+ * Executes the next {@link #befores} or {@link #actions} function. If {@link #stopped}
+ * is `true` or no functions are left to execute, the {@link #done} function will be called.
+ *
+ * @private
+ * @return {Ext.route.Action} this
+ */
+ next: function() {
+ var me = this,
+ actions = me.getActions(),
+ befores = me.getBefores(),
+ urlParams = me.getUrlParams(),
+ config, ret, args;
+ if (Ext.isArray(urlParams)) {
+ args = urlParams.slice();
+ } else {
+ args = [
+ urlParams
+ ];
+ }
+ if (me.stopped || (befores ? !befores.length : true) && (actions ? !actions.length : true)) {
+ me.done();
+ } else {
+ if (befores && befores.length) {
+ config = befores.shift();
+ args.push(me);
+ ret = Ext.callback(config.fn, config.scope, args);
+ if (ret && ret.then) {
+ ret.then(function(arg) {
+ me.resume(arg);
+ }, function(arg) {
+ me.stop(arg);
+ });
+ }
+ } else if (actions && actions.length) {
+ config = actions.shift();
+ Ext.callback(config.fn, config.scope, args);
+ me.next();
+ } else {
+ // needed?
+ me.next();
+ }
+ }
+ return me;
+ },
+ /**
+ * Starts the execution of {@link #befores} and/or {@link #actions} functions.
+ *
+ * @return {Ext.promise.Promise}
+ */
+ run: function() {
+ var deferred = this.deferred;
+ if (!this.started) {
+ this.next();
+ this.started = true;
+ }
+ return deferred.promise;
+ },
+ /**
+ * When no {@link #befores} or {@link #actions} functions are left to execute
+ * or {@link #stopped} is `true`, this function will be executed to resolve
+ * or reject the {@link #deferred} object.
+ *
+ * @private
+ * @return {Ext.route.Action} this
+ */
+ done: function() {
+ var deferred = this.deferred;
+ if (this.stopped) {
+ deferred.reject();
+ } else {
+ deferred.resolve();
+ }
+ this.destroy();
+ return this;
+ },
+ /**
+ * Add a function to the {@link #befores} stack.
+ *
+ * action.before(function() {}, this);
+ *
+ * By default, the function will be added to the end of the {@link #befores} stack. If
+ * instead the function should be placed at the beginning of the stack, you can pass
+ * `true` as the first argument:
+ *
+ * action.before(true, function() {}, this);
+ *
+ * @param {Boolean} [first=false] Pass `true` to add the function to the beginning of the
+ * {@link #befores} stack instead of the end.
+ * @param {Function/String} fn The function to add to the {@link #befores}.
+ * @param {Object} [scope] The scope of the function to execute with. This is normally
+ * the class that is adding the function to the before stack.
+ * @return {Ext.route.Action} this
+ */
+ before: function(first, fn, scope) {
+ if (!Ext.isBoolean(first)) {
+ scope = fn;
+ fn = first;
+ first = false;
+ }
+ // eslint-disable-next-line vars-on-top
+ var befores = this.getBefores(),
+ config = {
+ fn: fn,
+ scope: scope
+ };
+ if (this.destroyed) {
+ Ext.raise('This action has has already resolved and therefore will never ' + 'execute this function.');
+ return;
+ }
+ if (befores) {
+ if (first) {
+ befores.unshift(config);
+ } else {
+ befores.push(config);
+ }
+ } else {
+ this.setBefores(config);
+ }
+ return this;
+ },
+ /**
+ * Add a function to the {@link #actions} stack.
+ *
+ * action.action(function() {}, this);
+ *
+ * By default, the function will be added to the end of the {@link #actions} stack. If
+ * instead the function should be placed at the beginning of the stack, you can pass
+ * `true` as the first argument:
+ *
+ * action.action(true, function() {}, this);
+ *
+ * @param {Boolean} [first=false] Pass `true` to add the function to the beginning of the
+ * {@link #befores} stack.
+ * @param {Function/String} fn The function to add to the {@link #actions}.
+ * @param {Object} [scope] The scope of the function to execute with. This is normally
+ * the class that is adding the function to the action stack.
+ * @return {Ext.route.Action} this
+ */
+ action: function(first, fn, scope) {
+ if (!Ext.isBoolean(first)) {
+ scope = fn;
+ fn = first;
+ first = false;
+ }
+ // eslint-disable-next-line vars-on-top
+ var actions = this.getActions(),
+ config = {
+ fn: fn,
+ scope: scope
+ };
+ if (this.destroyed) {
+ Ext.raise('This action has has already resolved and therefore will never ' + 'execute this function.');
+ return;
+ }
+ if (actions) {
+ if (first) {
+ actions.unshift(config);
+ } else {
+ actions.push(config);
+ }
+ } else {
+ this.setActions(config);
+ }
+ return this;
+ },
+ /**
+ * Execute functions when this action has been resolved or rejected.
+ *
+ * @param {Function} resolve The function to execute when this action has been resolved.
+ * @param {Function} reject The function to execute when a before function stopped this action.
+ * @return {Ext.Promise}
+ */
+ then: function(resolve, reject) {
+ if (this.destroyed) {
+ Ext.raise('This action has has already resolved and therefore will never ' + 'execute either function.');
+ return;
+ }
+ return this.deferred.then(resolve, reject);
+ }
+});
+
+/**
+ * Enables reactive actions to handle changes in the hash by using the
+ * {@link Ext.route.Mixin#routes routes} configuration in a controller.
+ * An example configuration would be:
+ *
+ * Ext.define('MyApp.view.main.MainController', {
+ * extend: 'Ext.app.ViewController',
+ * alias: 'controller.app-main',
+ *
+ * routes: {
+ * 'user/:{id}': 'onUser'
+ * },
+ *
+ * onUser: function (values) {
+ * var id = values.id;
+ * // ...
+ * }
+ * });
+ *
+ * The `routes` object can also receive an object to further configure
+ * the route, for example you can configure a `before` action that will
+ * be executed before the `action` or can cancel the route execution:
+ *
+ * Ext.define('MyApp.view.main.MainController', {
+ * extend: 'Ext.app.ViewController',
+ * alias: 'controller.app-main',
+ *
+ * routes: {
+ * 'user/:{id}': {
+ * action: 'onUser',
+ * before: 'onBeforeUser',
+ * name: 'user'
+ * }
+ * },
+ *
+ * onBeforeUser: function (values) {
+ * return Ext.Ajax
+ * .request({
+ * url: '/check/permission',
+ * params: {
+ * route: 'user',
+ * meta: {
+ * id: values.id
+ * }
+ * }
+ * });
+ * },
+ *
+ * onUser: function (values) {
+ * var id = values.id;
+ * // ...
+ * }
+ * });
+ *
+ * URL Parameters in a route can also define a type that will be used
+ * when matching hashes when finding routes that recognize a hash and
+ * also parses the value into numbers:
+ *
+ * Ext.define('MyApp.view.main.MainController', {
+ * extend: 'Ext.app.ViewController',
+ * alias: 'controller.app-main',
+ *
+ * routes: {
+ * 'user/:{id:num}': {
+ * action: 'onUser',
+ * before: 'onBeforeUser',
+ * name: 'user'
+ * }
+ * },
+ *
+ * onBeforeUser: function (values) {
+ * return Ext.Ajax
+ * .request({
+ * url: '/check/permission',
+ * params: {
+ * route: 'user',
+ * meta: {
+ * id: values.id
+ * }
+ * }
+ * });
+ * },
+ *
+ * onUser: function (values) {
+ * var id = values.id;
+ * // ...
+ * }
+ * });
+ *
+ * In this example, the id parameter added `:num` to the parameter which
+ * will now mean the route will only recognize a value for the id parameter
+ * that is a number such as `#user/123` and will not recognize `#user/abc`.
+ * The id passed to the action and before handlers will also get cast into
+ * a number instead of a string. If a type is not provided, it will use
+ * the {@link #defaultMatcher default matcher}.
+ *
+ * For more on types, see the {@link #cfg!types} config.
+ *
+ * For backwards compatibility, there is `positional` mode which is like
+ * `named` mode but how you define the url parameters and how they are passed
+ * to the action and before handlers is slightly different:
+ *
+ * Ext.define('MyApp.view.main.MainController', {
+ * extend: 'Ext.app.ViewController',
+ * alias: 'controller.app-main',
+ *
+ * routes: {
+ * 'user/:id:action': {
+ * action: 'onUser',
+ * before: 'onBeforeUser',
+ * name: 'user',
+ * conditions: {
+ * ':action': '(edit|delete)?'
+ * }
+ * }
+ * },
+ *
+ * onBeforeUser: function (id, action) {
+ * return Ext.Ajax
+ * .request({
+ * url: '/check/permission',
+ * params: {
+ * route: 'user',
+ * meta: {
+ * action: action,
+ * id: id
+ * }
+ * }
+ * });
+ * },
+ *
+ * onUser: function (id) {
+ * // ...
+ * }
+ * });
+ *
+ * The parameters are defined without curly braces (`:id`, `:action`) and
+ * they are passed as individual arguments to the action and before handlers.
+ *
+ * It's important to note you cannot mix positional and named parameter formats
+ * in the same route since how they are passed to the handlers is different.
+ *
+ * Routes can define sections of a route pattern that are optional by surrounding
+ * the section that is to be optional with parenthesis. For example, if a route
+ * should match both `#user` and `#user/1234` to either show a grid of all users
+ * or details or a single user, you can define the route such as:
+ *
+ * Ext.define('MyApp.view.main.MainController', {
+ * extend: 'Ext.app.ViewController',
+ * alias: 'controller.app-main',
+ *
+ * routes: {
+ * 'user(/:{id:num})': {
+ * action: 'onUser',
+ * name: 'user'
+ * }
+ * },
+ *
+ * onUser: function (params) {
+ * if (params.id) {
+ * // load user details
+ * } else {
+ * // load grid of users
+ * }
+ * }
+ * });
+ */
+Ext.define('Ext.route.Route', {
+ /**
+ * @event beforeroute
+ * @member Ext.GlobalEvents
+ *
+ * Fires when a route is about to be executed. This allows pre-processing to add additional
+ * {@link Ext.route.Action#before before} or {@link Ext.route.Action#action action} handlers
+ * when the {@link Ext.route.Action Action} is run.
+ *
+ * The route can be prevented from executing by returning `false` in a listener
+ * or executing the {@link Ext.route.Action#stop stop} method on the action.
+ *
+ * @param {Ext.route.Route} route The route being executed.
+ * @param {Ext.route.Action} action The action that will be run.
+ */
+ /**
+ * @event beforerouteexit
+ * @member Ext.GlobalEvents
+ *
+ * Fires when a route is being exited meaning when a route
+ * was executed but no longer matches a token in the current hash.
+ *
+ * The exit handlers can be prevented from executing by returning `false` in a listener
+ * or executing the {@link Ext.route.Action#stop stop} method on the action.
+ *
+ * @param {Ext.route.Action} action The action with defined exit actions. Each
+ * action will execute with the last token this route was connected with.
+ * @param {Ext.route.Route} route
+ */
+ config: {
+ /**
+ * @cfg {String} name The name of this route. The name can be used when using
+ * {@link Ext.route.Mixin#redirectTo}.
+ */
+ name: null,
+ /**
+ * @cfg {String} url (required) The url regex to match against.
+ */
+ url: null,
+ /**
+ * @cfg {Boolean} [allowInactive=false] `true` to allow this route to be triggered on
+ * a controller that is not active.
+ */
+ allowInactive: false,
+ /**
+ * @cfg {Object} conditions
+ * Optional set of conditions for each token in the url string. Each key should
+ * be one of the tokens, each value should be a regex that the token should accept.
+ *
+ * For `positional` mode, if you have a route with a url like `'files/:fileName'` and
+ * you want it to match urls like `files/someImage.jpg` then you can set these
+ * conditions to allow the :fileName token to accept strings containing a period:
+ *
+ * conditions: {
+ * ':fileName': '([0-9a-zA-Z\.]+)'
+ * }
+ *
+ * For `named` mode, if you have a route with a url like `'files/:{fileName}'`
+ * and you want it to match urls like `files/someImage.jpg` then you can set these
+ * conditions to allow the :{fileName} token to accept strings containing a period:
+ *
+ * conditions: {
+ * 'fileName': '([0-9a-zA-Z\.]+)'
+ * }
+ *
+ * You can also define a condition to parse the value or even split it on a character:
+ *
+ * conditions: {
+ * 'fileName': {
+ * re: '([0-9a-zA-Z\.]+)',
+ * split: '.', // split the value so you get an array ['someImage', 'jpg']
+ * parse: function (values) {
+ * return values[0]; // return a string without the extension
+ * }
+ * }
+ * }
+ */
+ conditions: {},
+ /**
+ * @cfg {Boolean} [caseInsensitive=false] `true` to allow the tokens to be matched with
+ * case-insensitive.
+ */
+ caseInsensitive: false,
+ /**
+ * @cfg {Object[]} [handlers=[]]
+ * The array of connected handlers to this route. Each handler must defined a
+ * `scope` and can define an `action`, `before` and/or `exit` handler:
+ *
+ * handlers: [{
+ * action: function() {
+ * //...
+ * },
+ * scope: {}
+ * }, {
+ * action: function() {
+ * //...
+ * },
+ * before: function() {
+ * //...
+ * },
+ * scope: {}
+ * }, {
+ * exit: function() {
+ * //...
+ * },
+ * scope: {}
+ * }]
+ *
+ * The `action`, `before` and `exit` handlers can be a string that will be resolved
+ * from the `scope`:
+ *
+ * handlers: [{
+ * action: 'onAction',
+ * before: 'onBefore',
+ * exit: 'onExit',
+ * scope: {
+ * onAction: function () {
+ * //...
+ * },
+ * onBefore: function () {
+ * //...
+ * },
+ * onExit: function () {
+ * //...
+ * }
+ * }
+ * }]
+ */
+ handlers: [],
+ /* eslint-disable max-len */
+ /**
+ * @since 6.6.0
+ * @property {Object} types
+ * An object of types that will be used to match and parse values from a matched
+ * url. There are four default types:
+ *
+ * - `alpha` This will only match values that have only alpha characters using
+ * the regex `([a-zA-Z]+)`.
+ * - `alphanum` This will only match values that have alpha and numeric characters
+ * using the regex `([a-zA-Z0-9]+|[0-9]*(?:\\.[0-9]*)?)`. If a value is a number,
+ * which a number can have a period (`10.4`), the value will be case into a float
+ * using `parseFloat`.
+ * - `num` This will only match values that have numeric characters using the regex
+ * `([0-9]*(?:\\.[0-9]*)?)`. The value, which can have a period (`10.4`), will be
+ * case into a float using `parseFloat`.
+ * - `...` This is meant to be the last argument in the url and will match all
+ * characters using the regex `(.+)?`. If a value is matched, this is an optional
+ * type, the value will be split by `/` and an array will be sent to the handler
+ * methods. If no value was matched, the value will be `undefined`.
+ *
+ * When defining routes, a type is optional and will use the
+ * {@link #defaultMatcher default matcher} but the url parameter must be enclosed
+ * in curly braces which will send a single object to the route handlers:
+ *
+ * Ext.define('MyApp.view.MainController', {
+ * extend: 'Ext.app.ViewController',
+ * alias: 'controller.myapp-main',
+ *
+ * routes: {
+ * 'view/:{view}/:{child:alphanum}:{args...}': {
+ * action: 'onView',
+ * before: 'onBeforeView',
+ * name: 'view'
+ * }
+ * },
+ *
+ * onBeforeView: function (values) {
+ * return Ext.Ajax.request({
+ * url: 'check/permission',
+ * params: {
+ * view: values.view,
+ * info: { childView: values.child }
+ * }
+ * });
+ * },
+ *
+ * onView: function (values) {}
+ * });
+ *
+ * In this example, there are 3 parameters defined. The `:{view}` parameter has no
+ * type which will match characters using the {@link #defaultMatcher default matcher}
+ * but is required to be in the matched url. The `:{child:alphanum}` will only match
+ * characters that are alpha or numeric but is required to be in the matched url. The
+ * `:{args...}` is the only optional parameter in this route but can match any
+ * character and will be an array of values split by `/` unless there are no values
+ * in which case `undefined` will be sent in the object.
+ *
+ * If the hash is `#view/user/edit`, the `values` argument sent to the handlers would be:
+ *
+ * {
+ * view: 'user',
+ * child: 'edit',
+ * args: undefined
+ * }
+ *
+ * Since there were no more values for the `args` parameter, it's value is `undefined`.
+ *
+ * If the hash is `#view/user/1234`, the `values` argument sent to the handlers would be:
+ *
+ * {
+ * view: 'user',
+ * child: 1234,
+ * args: undefined
+ * }
+ *
+ * Notice the `child` value is a number instead of a string.
+ *
+ * If the hash is `#view/user/1234/edit/settings`, the `values` argument sent to the
+ * handlers would be:
+ *
+ * {
+ * view: 'user',
+ * child: 1234,
+ * args: ['edit', 'settings']
+ * }
+ *
+ * The `args` parameter matched the `edit/settings` and split it by the `/` producing
+ * the array.
+ *
+ * To add custom types, you can override `Ext.route.Route`:
+ *
+ * Ext.define('Override.route.Route', {
+ * override: 'Ext.route.Route',
+ *
+ * config: {
+ * types: {
+ * uuid: {
+ * re: '([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})'
+ * }
+ * }
+ * }
+ * });
+ *
+ * You can now use the `uuid` type in your routes:
+ *
+ * Ext.define('MyApp.view.MainController', {
+ * extend: 'Ext.app.ViewController',
+ * alias: 'controller.myapp-main',
+ *
+ * routes: {
+ * 'user/:{userid:uuid}': {
+ * action: 'onUser',
+ * caseInsensitive: true,
+ * name: 'user'
+ * }
+ * },
+ *
+ * onUser: function (values) {}
+ * });
+ *
+ * This would match if the hash was like `#user/C56A4180-65AA-42EC-A945-5FD21DEC0538`
+ * and the `values` object would then be:
+ *
+ * {
+ * user: 'C56A4180-65AA-42EC-A945-5FD21DEC0538'
+ * }
+ */
+ /* eslint-enable max-len */
+ types: {
+ cached: true,
+ $value: {
+ alpha: {
+ re: '([a-zA-Z]+)'
+ },
+ alphanum: {
+ re: '([a-zA-Z0-9]+|[0-9]+(?:\\.[0-9]+)?|[0-9]*(?:\\.[0-9]+){1})',
+ parse: function(value) {
+ var test;
+ if (value && this.numRe.test(value)) {
+ test = parseFloat(value);
+ if (!isNaN(test)) {
+ value = test;
+ }
+ }
+ return value;
+ }
+ },
+ num: {
+ // allow `1`, `10`, 10.1`, `.1`
+ re: '([0-9]+(?:\\.[0-9]+)?|[0-9]*(?:\\.[0-9]+){1})',
+ parse: function(value) {
+ if (value) {
+ value = parseFloat(value);
+ }
+ return value;
+ }
+ },
+ '...': {
+ re: '(.+)?',
+ split: '/',
+ parse: function(values) {
+ var length, i, value;
+ if (values) {
+ length = values.length;
+ for (i = 0; i < length; i++) {
+ value = parseFloat(values[i]);
+ if (!isNaN(value)) {
+ values[i] = value;
+ }
+ }
+ }
+ return values;
+ }
+ }
+ }
+ }
+ },
+ /**
+ * @property {String} [defaultMatcher='([%a-zA-Z0-9\\-\\_\\s,]+)'] The default RegExp string
+ * to use to match parameters with.
+ */
+ defaultMatcher: '([%a-zA-Z0-9\\-\\_\\s,]+)',
+ /**
+ * @private
+ * @property {RegExp} matcherRegex A regular expression to match the token to the
+ * configured {@link #url}.
+ */
+ /**
+ * @since 6.6.0
+ * @property {RegExp} numRe A regular expression to match against float numbers for
+ * `alphanum`, `num` and `...` {@link #cfg!types} in order to cast into floats.
+ */
+ numRe: /^[0-9]*(?:\.[0-9]*)?$/,
+ /**
+ * @private
+ * @since 6.6.0
+ * @property {RegExp} typeParamRegex
+ * A regular expression to determine if the parameter may contain type information.
+ * If a parameter does have type information, the url parameters sent to the
+ * {@link Ext.route.Handler#before} and {@link Ext.route.Handler#after} will
+ * be in an object instead of separate arguments.
+ */
+ typeParamRegex: /:{([0-9A-Za-z_]+)(?::?([0-9A-Za-z_]+|.{3})?)}/g,
+ /**
+ * @private
+ * @since 6.6.0
+ * @property {RegExp} optionalParamRegex
+ * A regular expression to find groups intended to be optional values within the
+ * hash. This means that if they are in the hash they will match and return the
+ * values present. But, if they are not and the rest of the hash matches, the route
+ * will still execute passing `undefined` as the values of any parameters
+ * within an optional group.
+ *
+ * routes: {
+ * 'user(\/:{id:num})': {
+ * action: 'onUser',
+ * name: 'user'
+ * }
+ * }
+ *
+ * In this example, the `id` parameter and the slash will be optional since they
+ * are wrapped in the parentheses. This route would execute if the hash is `#user`
+ * or `#user/1234`.
+ */
+ optionalGroupRegex: /\((.+?)\)/g,
+ /**
+ * @private
+ * @property {RegExp} paramMatchingRegex
+ * A regular expression to check if there are parameters in the configured
+ * {@link #url}.
+ */
+ paramMatchingRegex: /:([0-9A-Za-z_]+)/g,
+ /**
+ * @private
+ * @property {Array/Object} paramsInMatchString
+ * An array or object of parameters in the configured {@link #url}.
+ */
+ /**
+ * @private
+ * @since 6.6.0
+ * @property {String} mode
+ * The mode based on the {@link #cfg!url} pattern this route is configured with.
+ * Valid values are:
+ *
+ * - `positional` The {@link #cfg!url} was configured with the parameter format
+ * as `:param`. The values in the handler functions will be individual arguments.
+ * Example:
+ *
+ * Ext.define('MyApp.view.MainController', {
+ * extend: 'Ext.app.ViewController',
+ * alias: 'controller.myapp-main',
+ *
+ * routes: {
+ * 'view/:view/:child': {
+ * action: 'onView',
+ * before: 'onBeforeView',
+ * name: 'view'
+ * }
+ * },
+ *
+ * onBeforeView: function (view, child) {
+ * return Ext.Ajax.request({
+ * url: 'check/permission',
+ * params: {
+ * view: view,
+ * info: { childView: child }
+ * }
+ * });
+ * },
+ *
+ * onView: function (view, child) {}
+ * });
+ *
+ * The values from the matched url that the `view` route would execute with are
+ * separate arguments in the before and action handlers.
+ * - `named` The {@link #cfg!url} was configured with the parameter format as
+ * `:{param:type}` where the `:type` is optional. Example:
+ *
+ * Ext.define('MyApp.view.MainController', {
+ * extend: 'Ext.app.ViewController',
+ * alias: 'controller.myapp-main',
+ *
+ * routes: {
+ * 'view/:{view}/:{child:alphanum}': {
+ * action: 'onView',
+ * before: 'onBeforeView',
+ * name: 'view'
+ * }
+ * },
+ *
+ * onBeforeView: function (values) {
+ * return Ext.Ajax.request({
+ * url: 'check/permission',
+ * params: {
+ * view: values.view,
+ * info: { childView: values.child }
+ * }
+ * });
+ * },
+ *
+ * onView: function (values) {}
+ * });
+ *
+ * The values from the matched url the `view` route would execute with are collected
+ * into an object with the parameter name as the key and the associated value as
+ * the value. See {@link #cfg!types} for more about this named mode.
+ */
+ /**
+ * @protected
+ * @property {Boolean} isRoute
+ */
+ isRoute: true,
+ constructor: function(config) {
+ var me = this,
+ url;
+ this.initConfig(config);
+ url = me.getUrl().replace(me.optionalGroupRegex, function(match, middle) {
+ return '(?:' + middle + ')?';
+ });
+ if (url.match(me.typeParamRegex)) {
+ me.handleNamedPattern(url);
+ } else {
+ me.handlePositionalPattern(url);
+ }
+ },
+ /**
+ * @private
+ * @since 6.6.0
+ * Handles a pattern that will enable positional {@link #property!mode}.
+ *
+ * @param {String} url The url pattern.
+ */
+ handlePositionalPattern: function(url) {
+ var me = this;
+ me.paramsInMatchString = url.match(me.paramMatchingRegex) || [];
+ me.matcherRegex = me.createMatcherRegex(url);
+ me.mode = 'positional';
+ },
+ /**
+ * @private
+ * @since 6.6.0
+ * Handles a pattern that will enable named {@link #property!mode}.
+ *
+ * @param {String} url The url pattern.
+ */
+ handleNamedPattern: function(url) {
+ var me = this,
+ typeParamRegex = me.typeParamRegex,
+ conditions = me.getConditions(),
+ types = me.getTypes(),
+ defaultMatcher = me.defaultMatcher,
+ params = {},
+ re = url.replace(typeParamRegex, function(match, param, typeMatch) {
+ var type = typeMatch && types[typeMatch],
+ matcher = conditions[param] || type || defaultMatcher;
+ if (params[param]) {
+ Ext.raise('"' + param + '" already defined in route "' + url + '"');
+ }
+ if (typeMatch && !type) {
+ Ext.raise('Unknown parameter type "' + typeMatch + '" in route "' + url + '"');
+ }
+ if (Ext.isObject(matcher)) {
+ matcher = matcher.re;
+ }
+ params[param] = {
+ matcher: matcher,
+ type: typeMatch
+ };
+ return matcher;
+ });
+ if (re.search(me.paramMatchingRegex) !== -1) {
+ Ext.raise('URL parameter mismatch. Positional url parameter found ' + 'while in named mode.');
+ }
+ me.paramsInMatchString = params;
+ me.matcherRegex = new RegExp('^' + re + '$', me.getCaseInsensitive() ? 'i' : '');
+ me.mode = 'named';
+ },
+ /**
+ * Attempts to recognize a given url string and return a meta data object including
+ * any URL parameter matches.
+ *
+ * @param {String} url The url to recognize.
+ * @return {Object/Boolean} The matched data, or `false` if no match.
+ */
+ recognize: function(url) {
+ var me = this,
+ recognized = me.recognizes(url),
+ handlers, length, hasHandler, handler, matches, urlParams, i;
+ if (recognized) {
+ handlers = me.getHandlers();
+ length = handlers.length;
+ for (i = 0; i < length; i++) {
+ handler = handlers[i];
+ if (handler.lastToken !== url) {
+ // there is a handler that can execute
+ hasHandler = true;
+ break;
+ }
+ }
+ if (!hasHandler && url === me.lastToken) {
+ // url matched the lastToken
+ return true;
+ }
+ // backwards compat
+ matches = me.matchesFor(url);
+ urlParams = me.getUrlParams(url);
+ return Ext.applyIf(matches, {
+ historyUrl: url,
+ urlParams: urlParams
+ });
+ }
+ return false;
+ },
+ /**
+ * @private
+ * @since 6.6.0
+ * Returns the url parameters matched in the given url.
+ *
+ * @param {String} url The url this route is executing on.
+ * @return {Array/Object} If {@link #property!mode} is `named`,
+ * an object from {@link #method!getNamedUrlParams} will be returned.
+ * If is `positional`, an array from {@link #method!getPositionalUrlParams}
+ * will be returned.
+ */
+ getUrlParams: function(url) {
+ if (this.mode === 'named') {
+ return this.getNamedUrlParams(url);
+ } else {
+ return this.getPositionalUrlParams(url);
+ }
+ },
+ /**
+ * @private
+ * @since 6.6.0
+ * Returns an array of url parameters values in order they appear in the url.
+ *
+ * @param {String} url The url the route is executing on.
+ * @return {Array}
+ */
+ getPositionalUrlParams: function(url) {
+ var params = [],
+ conditions = this.getConditions(),
+ keys = this.paramsInMatchString,
+ values = url.match(this.matcherRegex),
+ length = keys.length,
+ i, key, type, value;
+ // remove the full match
+ values.shift();
+ for (i = 0; i < length; i++) {
+ key = keys[i];
+ value = values[i];
+ if (conditions[key]) {
+ type = conditions[key];
+ } else if (key[0] === ':') {
+ key = key.substr(1);
+ if (conditions[key]) {
+ type = conditions[key];
+ }
+ }
+ value = this.parseValue(value, type);
+ if (Ext.isDefined(value) && value !== '') {
+ if (Ext.isArray(value)) {
+ params.push.apply(params, value);
+ } else {
+ params.push(value);
+ }
+ }
+ }
+ return params;
+ },
+ /**
+ * @private
+ * @since 6.6.0
+ * Returns an object of url parameters with parameter name as the
+ * object key and the value.
+ *
+ * @param {String} url The url the route is executing on.
+ * @return {Array}
+ */
+ getNamedUrlParams: function(url) {
+ var conditions = this.getConditions(),
+ types = this.getTypes(),
+ params = {},
+ keys = this.paramsInMatchString,
+ values = url.match(this.matcherRegex),
+ name, obj, value, type, condition;
+ // remove the full match
+ values.shift();
+ for (name in keys) {
+ obj = keys[name];
+ value = values.shift();
+ condition = conditions[name];
+ type = types[obj.type];
+ if (condition || type) {
+ type = Ext.merge({}, condition, types[obj.type]);
+ }
+ params[name] = this.parseValue(value, type);
+ }
+ return params;
+ },
+ /**
+ * @private
+ * @since 6.6.0
+ * Parses the value from the url with a {@link #cfg!types type}
+ * or a matching {@link #cfg!conditions condition}.
+ *
+ * @param {String} value The value from the url.
+ * @param {Object} [type] The type object that will be used to parse the value.
+ * @return {String/Number/Array}
+ */
+ parseValue: function(value, type) {
+ if (type) {
+ if (value && type.split) {
+ value = value.split(type.split);
+ // If first is empty string, remove.
+ // This could be because the value prior
+ // was `/foo/bar` which would lead to
+ // `['', 'foo', 'bar']`.
+ if (!value[0]) {
+ value.shift();
+ }
+ // If last is empty string, remove.
+ // This could be because the value prior
+ // was `foo/bar/` which would lead to
+ // `['foo', 'bar', '']`.
+ if (!value[value.length - 1]) {
+ value.pop();
+ }
+ }
+ if (type.parse) {
+ value = type.parse.call(this, value);
+ }
+ }
+ if (!value && Ext.isString(value)) {
+ // IE8 may have values as an empty string
+ // if there was no value that was matched
+ value = undefined;
+ }
+ return value;
+ },
+ /**
+ * Returns `true` if this {@link Ext.route.Route} matches the given url string.
+ *
+ * @param {String} url The url to test.
+ * @return {Boolean} `true` if this {@link Ext.route.Route} recognizes the url.
+ */
+ recognizes: function(url) {
+ return this.matcherRegex.test(url);
+ },
+ /**
+ * The method to execute the action using the configured before function which will
+ * kick off the actual {@link #actions} on the {@link #controller}.
+ *
+ * @param {String} token The token this route is being executed with.
+ * @param {Object} argConfig The object from the {@link Ext.route.Route}'s
+ * recognize method call.
+ * @return {Ext.promise.Promise}
+ */
+ execute: function(token, argConfig) {
+ var me = this,
+ allowInactive = me.getAllowInactive(),
+ handlers = me.getHandlers(),
+ queue = Ext.route.Router.getQueueRoutes(),
+ length = handlers.length,
+ urlParams = (argConfig && argConfig.urlParams) || [],
+ i, handler, scope, action, promises, single, remover;
+ me.lastToken = token;
+ if (!queue) {
+ promises = [];
+ }
+ return new Ext.Promise(function(resolve, reject) {
+ if (argConfig === false) {
+ reject();
+ } else {
+ if (queue) {
+ action = new Ext.route.Action({
+ urlParams: urlParams
+ });
+ }
+ for (i = 0; i < length; i++) {
+ handler = handlers[i];
+ if (token != null && handler.lastToken === token) {
+ // no change on this handler
+
+ continue;
+ }
+ scope = handler.scope;
+ handler.lastToken = token;
+ if (!allowInactive && scope.isActive && !scope.isActive()) {
+
+ continue;
+ }
+ if (!queue) {
+ action = new Ext.route.Action({
+ urlParams: urlParams
+ });
+ }
+ single = handler.single;
+ if (handler.before) {
+ action.before(handler.before, scope);
+ }
+ if (handler.action) {
+ action.action(handler.action, scope);
+ }
+ if (single) {
+ remover = Ext.bind(me.removeHandler, me, [
+ null,
+ handler
+ ]);
+ if (single === true) {
+ if (handler.action) {
+ action.action(remover, me);
+ } else {
+ action.before(function() {
+ remover();
+ return Ext.Promise.resolve();
+ }, me);
+ }
+ } else {
+ // all before actions have to resolve,
+ // resolve a promise to allow the action
+ // chain to continue
+ action.before(single === 'before', function() {
+ remover();
+ return Ext.Promise.resolve();
+ }, me);
+ }
+ }
+ if (!queue) {
+ if (Ext.fireEvent('beforeroute', action, me) === false) {
+ action.destroy();
+ } else {
+ promises.push(action.run());
+ }
+ }
+ }
+ if (queue) {
+ if (Ext.fireEvent('beforeroute', action, me) === false) {
+ action.destroy();
+ reject();
+ } else {
+ action.run().then(resolve, reject);
+ }
+ } else {
+ Ext.Promise.all(promises).then(resolve, reject);
+ }
+ }
+ });
+ },
+ /**
+ * Returns a hash of matching url segments for the given url.
+ *
+ * @param {String} url The url to extract matches for
+ * @return {Object} matching url segments
+ */
+ matchesFor: function(url) {
+ var params = {},
+ keys = this.mode === 'named' ? Ext.Object.getKeys(this.paramsInMatchString) : this.paramsInMatchString,
+ values = url.match(this.matcherRegex),
+ length = keys.length,
+ i;
+ // first value is the entire match so reject
+ values.shift();
+ for (i = 0; i < length; i++) {
+ params[keys[i].replace(':', '')] = values[i];
+ }
+ return params;
+ },
+ /**
+ * Takes the configured url string including wildcards and returns a regex that can be
+ * used to match against a url.
+ *
+ * This is only used in `positional` {@link #property!mode}.
+ *
+ * @param {String} url The url string.
+ * @return {RegExp} The matcher regex.
+ */
+ createMatcherRegex: function(url) {
+ // Converts a route string into an array of symbols starting with a colon. e.g.
+ // ":controller/:action/:id" => [':controller', ':action', ':id']
+ var me = this,
+ paramsInMatchString = me.paramsInMatchString,
+ conditions = me.getConditions(),
+ defaultMatcher = me.defaultMatcher,
+ length = paramsInMatchString.length,
+ modifiers = me.getCaseInsensitive() ? 'i' : '',
+ i, param, matcher;
+ if (url === '*') {
+ // handle wildcard routes, won't have conditions
+ url = url.replace('*', '\\*');
+ } else {
+ for (i = 0; i < length; i++) {
+ param = paramsInMatchString[i];
+ // Even if the param is a named param, we need to
+ // allow "local" overriding.
+ if (conditions[param]) {
+ matcher = conditions[param];
+ }
+ // without colon
+ else if (param[0] === ':' && conditions[param.substr(1)]) {
+ matcher = conditions[param.substr(1)];
+ } else {
+ matcher = defaultMatcher;
+ }
+ if (Ext.isObject(matcher)) {
+ matcher = matcher.re;
+ }
+ url = url.replace(new RegExp(param), matcher || defaultMatcher);
+ }
+ }
+ // we want to match the whole string, so include the anchors
+ return new RegExp('^' + url + '$', modifiers);
+ },
+ /**
+ * Adds a handler to the {@link #cfg!handlers} stack.
+ *
+ * @param {Object} handler
+ * An object to describe the handler. A handler should define a `fn` and `scope`.
+ * If the `fn` is a String, the function will be resolved from the `scope`.
+ * @return {Ext.route.Route} this
+ */
+ addHandler: function(handler) {
+ var handlers = this.getHandlers();
+ if (!handler.isInstance) {
+ handler = new Ext.route.Handler(handler);
+ }
+ handlers.push(handler);
+ return handler.route = this;
+ },
+ /**
+ * Removes a handler from the {@link #cfg!handlers} stack. This normally happens when
+ * destroying a class instance.
+ *
+ * @param {Object/Ext.Base} scope The class instance to match handlers with.
+ * @param {Ext.route.Handler} [handler] An optional {@link Ext.route.Handler Handler}
+ * to only remove from the array of handlers. If no handler is passed, all handlers
+ * will be removed.
+ * @return {Ext.route.Route} this
+ */
+ removeHandler: function(scope, handler) {
+ var handlers = this.getHandlers(),
+ length = handlers.length,
+ newHandlers = [],
+ i, item;
+ for (i = 0; i < length; i++) {
+ item = handlers[i];
+ if (handler) {
+ if (item !== handler) {
+ newHandlers.push(item);
+ }
+ } else if (item.scope !== scope) {
+ newHandlers.push(item);
+ }
+ }
+ this.setHandlers(newHandlers);
+ return this;
+ },
+ /**
+ * Clears the last token properties of this route and all handlers.
+ */
+ clearLastTokens: function() {
+ var handlers = this.getHandlers(),
+ length = handlers.length,
+ i;
+ for (i = 0; i < length; i++) {
+ handlers[i].lastToken = null;
+ }
+ this.lastToken = null;
+ },
+ /**
+ * @private
+ * @since 6.6.0
+ *
+ * When a route is exited (no longer recognizes a token in the current hash)
+ * we need to clear all last tokens and execute any exit handlers.
+ */
+ onExit: function() {
+ var me = this,
+ handlers = me.getHandlers(),
+ allowInactive = me.getAllowInactive(),
+ length = handlers.length,
+ action = new Ext.route.Action({
+ urlParams: [
+ me.lastToken
+ ]
+ }),
+ i, handler, scope;
+ // Need to reset handlers' `lastToken` so that when a token
+ // is added to the document fragment it will not be falsely
+ // matched.
+ me.clearLastTokens();
+ for (i = 0; i < length; i++) {
+ handler = handlers[i];
+ if (handler.exit) {
+ scope = handler.scope;
+ if (!allowInactive && scope.isActive && !scope.isActive()) {
+
+ continue;
+ }
+ action.action(handler.exit, scope);
+ }
+ }
+ if (Ext.fireEvent('beforerouteexit', action, me) === false) {
+ action.destroy();
+ } else {
+ action.run();
+ }
+ }
+});
+
+/**
+ * A Ext.mixin.Observable subclass that is provided for backward compatibility.
+ * Applications should avoid using this class, and use Ext.mixin.Observable instead.
+ */
+Ext.define('Ext.util.Observable', {
+ extend: Ext.mixin.Observable,
+ // The constructor of Ext.util.Observable instances processes the config object by
+ // calling Ext.apply(this, config); instead of this.initConfig(config);
+ $applyConfigs: true
+}, function(Observable) {
+ var Super = Ext.mixin.Observable;
+ /**
+ * @method releaseCapture
+ * @static
+ * @inheritdoc Ext.mixin.Observable#releaseCapture
+ */
+ Observable.releaseCapture = Super.releaseCapture;
+ /**
+ * @method capture
+ * @static
+ * @inheritdoc Ext.mixin.Observable#capture
+ */
+ Observable.capture = Super.capture;
+ /**
+ * @member Ext.util.Observable
+ * @private
+ */
+ Observable.captureArgs = Super.captureArgs;
+ /**
+ * @method observe
+ * @static
+ * @inheritdoc Ext.mixin.Observable#observe
+ */
+ Observable.observe = Observable.observeClass = Super.observe;
+});
+
+/**
+ * History management component that allows you to register arbitrary tokens that signify
+ * application history state on navigation actions. You can then handle the history
+ * {@link #change} event in order to reset your application UI to the appropriate state when
+ * the user navigates forward or backward through the browser history stack.
+ *
+ * ## Initializing
+ *
+ * The {@link #init} method of the History object must be called before using History. This sets up
+ * the internal state and must be the first thing called before using History.
+ */
+Ext.define('Ext.util.History', {
+ singleton: true,
+ alternateClassName: 'Ext.History',
+ mixins: {
+ observable: Ext.util.Observable
+ },
+ /**
+ * @property
+ * True to use `window.top.location.hash` or false to use `window.location.hash`. Must be set
+ * before {@link #init} is called because the `hashchange` event listener is added to the window
+ * at initialization time.
+ */
+ useTopWindow: false,
+ /**
+ * @property {Boolean} hashbang If set to `true`, when a hash is set, the hash will be prefixed
+ * with an exclamation making it a hash bang instead of just a hash.
+ *
+ * Ext.util.History.add('foo'); // will result in #foo
+ *
+ * Ext.util.History.hashbang = true;
+ * Ext.util.History.add('bar'); // will result in #!bar
+ */
+ /**
+ * @property {String} currentToken The current token.
+ * @private
+ */
+ /**
+ * @event ready
+ * Fires when the Ext.util.History singleton has been initialized and is ready for use.
+ * @param {Ext.util.History} history The Ext.util.History singleton.
+ */
+ /**
+ * @event change
+ * Fires when navigation back or forwards within the local page's history occurs.
+ * @param {String} token An identifier associated with the page state at that point
+ * in its history.
+ */
+ hashRe: /^(#?!?)/,
+ constructor: function() {
+ var me = this;
+ me.ready = false;
+ me.currentToken = null;
+ me.mixins.observable.constructor.call(me);
+ },
+ /**
+ * Gets the actual hash from the url. This shouldn't need to be used directly but use the
+ * {@link #getToken} method instead.
+ *
+ * @return {String} The hash from the window object.
+ * @private
+ */
+ getHash: function() {
+ return (this.win.location.hash || '').replace(this.hashRe, '');
+ },
+ /**
+ * Updates the hash on the window. This shouldn't need to be used directly but use the
+ * {@link #add} method instead.
+ *
+ * @param {String} hash The hash to use
+ * @param {Boolean} replace If `true`, the hash passed in will replace the current resource
+ * by using the `location.replace()` API.
+ * @private
+ */
+ setHash: function(hash, replace) {
+ var me = this,
+ hashRe = me.hashRe,
+ loc = me.win.location;
+ // may or may not already be prefixed with # or #! already
+ hash = hash.replace(hashRe, me.hashbang ? '#!' : '#');
+ try {
+ if (replace) {
+ loc.replace(hash);
+ } else {
+ loc.hash = hash;
+ }
+ // need to make sure currentToken is not prefixed
+ me.currentToken = hash.replace(hashRe, '');
+ } catch (e) {}
+ },
+ // IE can give Access Denied (esp. in popup windows)
+ /**
+ * Handles when the hash in the URL has been updated. Will also fired the change event.
+ *
+ * @param {String} token The token that was changed to
+ * @private
+ */
+ handleStateChange: function(token) {
+ // browser won't have # here but may have !
+ token = token.replace(this.hashRe, '');
+ this.fireEvent('change', this.currentToken = token);
+ },
+ /**
+ * Bootstraps the initialization the location.hash.
+ * @private
+ */
+ startUp: function() {
+ var me = this;
+ me.currentToken = me.getHash();
+ Ext.get(me.win).on('hashchange', me.onHashChange, me);
+ me.ready = true;
+ me.fireEvent('ready', me);
+ },
+ onHashChange: function() {
+ var me = this,
+ newHash = me.getHash();
+ if (newHash !== me.hash) {
+ me.hash = newHash;
+ me.handleStateChange(newHash);
+ }
+ },
+ /**
+ * Initializes the global History instance.
+ * @param {Function} [onReady] A callback function that will be called once the history
+ * component is fully initialized.
+ * @param {Object} [scope] The scope (`this` reference) in which the callback is executed.
+ * Defaults to the browser window.
+ */
+ init: function(onReady, scope) {
+ var me = this;
+ if (me.ready) {
+ Ext.callback(onReady, scope, [
+ me
+ ]);
+ return;
+ }
+ if (!Ext.isReady) {
+ Ext.onInternalReady(function() {
+ me.init(onReady, scope);
+ });
+ return;
+ }
+ me.win = me.useTopWindow ? window.top : window;
+ me.hash = me.getHash();
+ if (onReady) {
+ me.on('ready', onReady, scope, {
+ single: true
+ });
+ }
+ me.startUp();
+ },
+ /**
+ * Add a new token to the history stack. This can be any arbitrary value, although it would
+ * commonly be the concatenation of a component id and another id marking the specific history
+ * state of that component. Example usage:
+ *
+ * // Handle tab changes on a TabPanel
+ * tabPanel.on('tabchange', function(tabPanel, tab){
+ * Ext.History.add(tabPanel.id + ':' + tab.id);
+ * });
+ *
+ * @param {String} token The value that defines a particular application-specific history state
+ * @param {Boolean} [preventDuplicates=true] When true, if the passed token matches the current
+ * token it will not save a new history step. Set to false if the same state can be saved more
+ * than once at the same history stack location.
+ *
+ * @return {Boolean} Whether the token was set in the case if the current token matches
+ * the token passed.
+ */
+ add: function(token, preventDuplicates) {
+ var me = this,
+ set = false;
+ if (preventDuplicates === false || me.getToken() !== token) {
+ me.setHash(token);
+ set = true;
+ }
+ return set;
+ },
+ /**
+ * Replaces the current resource in history.
+ *
+ * @param {String} token The value that will replace the current resource in the history state.
+ * @param {Boolean} [preventDuplicates=true] When `true`, if the passed token matches
+ * the current token it will not save a new history step. Set to `false` if the same state
+ * can be saved more than once at the same history stack location.
+ *
+ * @return {Boolean} Whether the token was set in the case if the current token matches
+ * the token passed.
+ */
+ replace: function(token, preventDuplicates) {
+ var me = this,
+ set = false;
+ if (preventDuplicates === false || me.getToken() !== token) {
+ this.setHash(token, true);
+ set = true;
+ }
+ return set;
+ },
+ /**
+ * Programmatically steps back one step in browser history (equivalent to the user pressing
+ * the Back button).
+ */
+ back: function() {
+ this.win.history.go(-1);
+ },
+ /**
+ * Programmatically steps forward one step in browser history (equivalent to the user pressing
+ * the Forward button).
+ */
+ forward: function() {
+ this.win.history.go(1);
+ },
+ /**
+ * Retrieves the currently-active history token.
+ * @return {String} The token
+ */
+ getToken: function() {
+ return this.ready ? this.currentToken : this.getHash();
+ }
+});
+
+/**
+ * The Router is an ordered set of {@link Ext.route.Route} definitions that decode a
+ * url into a controller function to execute. Each `route` defines a type of url to match,
+ * along with the controller function to call if it is matched. The Router uses the
+ * {@link Ext.util.History} singleton to find out when the browser's url has changed.
+ *
+ * Routes are almost always defined inside a {@link Ext.Controller Controller}, as
+ * opposed to on the Router itself. End-developers should not usually need to interact
+ * directly with the Router as the Controllers manage everything automatically. See the
+ * {@link Ext.Controller Controller documentation} for more information on specifying
+ * routes.
+ */
+Ext.define('Ext.route.Router', {
+ singleton: true,
+ /**
+ * @event beforeroutes
+ * @member Ext.GlobalEvents
+ *
+ * Fires when the hash has changed and before any routes are executed. This allows
+ * pre-processing to add additional {@link Ext.route.Action#before before} or
+ * {@link Ext.route.Action#action action} handlers when the
+ * {@link Ext.route.Action Action} is run.
+ *
+ * Route execution can be prevented by returning `false` in the listener
+ * or executing the {@link Ext.route.Action#stop stop} method on the action.
+ *
+ * @param {Ext.route.Action} action An action that will be executed
+ * prior to any route execution.
+ * @param {String[]} tokens An array of individual tokens in the hash.
+ */
+ /**
+ * @event routereject
+ * @member Ext.GlobalEvents
+ *
+ * Fires when a route was rejected from either a before action,
+ * {@link Ext.GlobalEvents#beforeroutes} event or {@link Ext.GlobalEvents#beforeroute} event.
+ *
+ * @param {Ext.route.Route} route The route which had it's execution rejected.
+ */
+ config: {
+ /**
+ * @cfg {Boolean} hashBang Sets {@link Ext.util.History#hashbang} to enable/disable
+ * hashbang support.
+ */
+ hashbang: null,
+ /**
+ * @cfg {String} [multipleToken=|] The token to split the routes to support multiple routes.
+ */
+ multipleToken: '|',
+ /**
+ * @cfg {Boolean} [queueRoutes=true] `true` to queue routes to be executed one after the
+ * other, false to execute routes immediately.
+ */
+ queueRoutes: true
+ },
+ /**
+ * @property {Object} routes The connected {@link Ext.route.Route}
+ * instances.
+ */
+ /**
+ * @property {Boolean} isSuspended `true` if the router is currently suspended.
+ */
+ constructor: function() {
+ var History = Ext.util.History;
+ if (!History.ready) {
+ History.init();
+ }
+ History.on('change', this.onStateChange, this);
+ this.initConfig();
+ this.clear();
+ },
+ updateHashbang: function(hashbang) {
+ Ext.util.History.hashbang = hashbang;
+ },
+ /**
+ * React to a token
+ *
+ * @private
+ * @param {String} token The token to react to.
+ */
+ onStateChange: function(token) {
+ var me = this,
+ tokens = token.split(me.getMultipleToken()),
+ queue, i, length;
+ if (me.isSuspended) {
+ queue = me.suspendedQueue;
+ i = 0;
+ length = tokens.length;
+ if (queue) {
+ for (; i < length; i++) {
+ token = tokens[i];
+ // shouldn't keep track of duplicates
+ if (!Ext.Array.contains(queue, token)) {
+ queue.push(token);
+ }
+ }
+ }
+ } else {
+ me.handleBefore(tokens);
+ }
+ },
+ /**
+ * Fires the {@link Ext.GlobalEvents#beforeroutes} event and if
+ * `false` is returned can prevent any routes from executing.
+ *
+ * @private
+ * @param {String[]} tokens The individual tokens that were split from the hash
+ * using {@link #multipleToken}.
+ */
+ handleBefore: function(tokens) {
+ var me = this,
+ action = new Ext.route.Action();
+ if (Ext.fireEvent('beforeroutes', action, tokens) === false) {
+ action.destroy();
+ } else {
+ action.run().then(me.handleBeforeRoute.bind(me, tokens), Ext.emptyFn);
+ }
+ },
+ /**
+ * If a wildcard route was connected, that route needs to execute prior
+ * to any other route.
+ *
+ * @private
+ * @param {String[]} tokens The individual tokens that were split from the hash
+ * using {@link #multipleToken}.
+ */
+ handleBeforeRoute: function(tokens) {
+ var me = this,
+ beforeRoute = me.getByName('*');
+ if (beforeRoute) {
+ beforeRoute.execute().then(me.doRun.bind(me, tokens), Ext.emptyFn);
+ } else {
+ // no befores, go ahead with route determination
+ me.doRun(tokens);
+ }
+ },
+ /**
+ * Find routes that recognize one of the tokens in the document fragment
+ * and then exeucte the routes.
+ *
+ * @private
+ * @param {String[]} tokens The individual tokens that were split from the hash
+ * using {@link #multipleToken}.
+ */
+ doRun: function(tokens) {
+ var me = this,
+ app = me.application,
+ routes = me.routes,
+ i = 0,
+ length = tokens.length,
+ matched = {},
+ unmatched = [],
+ token, found, name, route, recognize;
+ for (; i < length; i++) {
+ token = tokens[i];
+ found = false;
+ for (name in routes) {
+ route = routes[name];
+ recognize = route.recognize(token);
+ if (recognize) {
+ found = true;
+ if (recognize !== true) {
+ // The document fragment may have changed but the token
+ // part that the route recognized did not change. Therefore
+ // is was matched but we should not execute the route again.
+ route.execute(token, recognize).then(null, Ext.bind(me.onRouteRejection, me, [
+ route
+ ], 0));
+ }
+ Ext.Array.remove(unmatched, route);
+ if (!matched[name]) {
+ matched[name] = 1;
+ }
+ } else if (!matched[name]) {
+ unmatched.push(route);
+ }
+ }
+ if (!found) {
+ if (app) {
+ // backwards compat
+ app.fireEvent('unmatchedroute', token);
+ }
+ Ext.fireEvent('unmatchedroute', token);
+ }
+ }
+ i = 0;
+ length = unmatched.length;
+ for (; i < length; i++) {
+ unmatched[i].onExit();
+ }
+ },
+ /**
+ * @private
+ * Called when a route was rejected.
+ */
+ onRouteRejection: function(route, error) {
+ Ext.fireEvent('routereject', route, error);
+ if (error) {
+ Ext.raise(error);
+ }
+ },
+ /**
+ * Create the {@link Ext.route.Route} instance and connect to the
+ * {@link Ext.route.Router} singleton.
+ *
+ * @param {String} url The url to recognize.
+ * @param {String} config The config on the controller to execute when the url is
+ * matched.
+ * @param {Ext.Base} instance The class instance associated with the
+ * {@link Ext.route.Route}
+ * @return {Ext.route.Handler} The handler that was added.
+ */
+ connect: function(url, config, instance) {
+ var routes = this.routes,
+ delimiter = this.getMultipleToken(),
+ name = config.name || url,
+ handler, route;
+ if (url[0] === '!') {
+ if (!Ext.util.History.hashbang) {
+ Ext.log({
+ level: 'error',
+ msg: 'Route found with "!" ("' + url + '"). Should use new hashbang functionality instead. ' + 'Please see the router guide for more: https://docs.sencha.com/extjs/' + Ext.getVersion().version + '/guides/application_architecture/router.html'
+ });
+ }
+ url = url.substr(1);
+ this.setHashbang(true);
+ }
+ if (Ext.isString(config)) {
+ config = {
+ action: config
+ };
+ }
+ handler = Ext.route.Handler.fromRouteConfig(config, instance);
+ route = routes[name];
+ if (!route) {
+ config.name = name;
+ config.url = url;
+ route = routes[name] = new Ext.route.Route(config);
+ }
+ route.addHandler(handler);
+ if (handler.lazy) {
+ // eslint-disable-next-line vars-on-top
+ var currentHash = Ext.util.History.getToken(),
+ tokens = currentHash.split(delimiter),
+ length = tokens.length,
+ matched = [],
+ i, token;
+ for (i = 0; i < length; i++) {
+ token = tokens[i];
+ if (Ext.Array.indexOf(matched, token) === -1 && route.recognize(token)) {
+ matched.push(token);
+ }
+ }
+ this.onStateChange(matched.join(delimiter));
+ }
+ return handler;
+ },
+ /**
+ * Disconnects all route handlers for a class instance.
+ *
+ * @param {Ext.Base} instance The class instance to disconnect route handlers from.
+ * @param {Object/Ext.route.Handler} [config]
+ * An optional config object to match a handler for. This will check all route
+ * handlers connected to the instance for match based on the action and before
+ * configurations. This can also be the actual {@link Ext.route.Handler handler}
+ * instance.
+ */
+ disconnect: function(instance, config) {
+ var routes = this.routes,
+ route, name;
+ if (config) {
+ route = config.route || this.getByName(config.name || config.url);
+ if (route) {
+ route.removeHandler(instance, config);
+ }
+ } else {
+ for (name in routes) {
+ route = routes[name];
+ route.removeHandler(instance);
+ }
+ }
+ },
+ /**
+ * Recognizes a url string connected to the Router, return the controller/action pair
+ * plus any additional config associated with it.
+ *
+ * @param {String} url The url to recognize.
+ * @return {Object/Boolean} If the url was recognized, the controller and action to
+ * call, else `false`.
+ */
+ recognize: function(url) {
+ var routes = this.routes,
+ matches = [],
+ name, arr, i, length, route, urlParams;
+ for (name in routes) {
+ arr = routes[name];
+ length = arr && arr.length;
+ if (length) {
+ i = 0;
+ for (; i < length; i++) {
+ route = arr[i];
+ urlParams = route.recognize(url);
+ if (urlParams) {
+ matches.push({
+ route: route,
+ urlParams: urlParams
+ });
+ }
+ }
+ }
+ }
+ return matches.length ? matches : false;
+ },
+ /**
+ * Convenience method which just calls the supplied function with the
+ * {@link Ext.route.Router} singleton. Example usage:
+ *
+ * Ext.route.Router.draw(function(map) {
+ * map.connect('activate/:token', {controller: 'users', action: 'activate'});
+ * map.connect('home', {controller: 'index', action: 'home'});
+ * });
+ *
+ * @param {Function} fn The function to call
+ */
+ draw: function(fn) {
+ fn.call(this, this);
+ },
+ /**
+ * Clear all the recognized routes.
+ */
+ clear: function() {
+ this.routes = {};
+ },
+ /**
+ * Resets the connected routes' last token they were executed on.
+ * @param {String} [token] If passed, only clear matching routes.
+ * @private
+ */
+ clearLastTokens: function(token) {
+ var routes = this.routes,
+ name, route;
+ for (name in routes) {
+ route = routes[name];
+ if (!token || route.recognize(token)) {
+ route.clearLastTokens();
+ }
+ }
+ },
+ /**
+ * Gets all routes by {@link Ext.route.Route#name}.
+ *
+ * @return {Ext.route.Route[]} If no routes found, `undefined` will be returned otherwise
+ * the array of {@link Ext.route.Route Routes} will be returned.
+ */
+ getByName: function(name) {
+ var routes = this.routes;
+ if (routes) {
+ return routes[name];
+ }
+ },
+ /**
+ * Suspends the handling of tokens (see {@link #resume}).
+ *
+ * @param {Boolean} [trackTokens] `false` to prevent any tokens to be
+ * queued while being suspended.
+ */
+ suspend: function(trackTokens) {
+ this.isSuspended = true;
+ if (!this.suspendedQueue && trackTokens !== false) {
+ this.suspendedQueue = [];
+ }
+ },
+ /**
+ * Resumes the execution of routes (see {@link #suspend}).
+ *
+ * @param {Boolean} [discardQueue] `true` to prevent any previously queued
+ * tokens from being enacted on.
+ */
+ resume: function(discardQueue) {
+ var me = this,
+ queue = me.suspendedQueue,
+ token;
+ if (me.isSuspended) {
+ me.isSuspended = false;
+ me.suspendedQueue = null;
+ if (!discardQueue && queue) {
+ token = queue.join(me.getMultipleToken());
+ me.onStateChange(token);
+ }
+ }
+ }
+});
+
+/**
+ * A mixin to allow any class to configure and listen to routes and also change the hash.
+ */
+Ext.define('Ext.route.Mixin', {
+ extend: Ext.Mixin,
+ mixinConfig: {
+ id: 'routerable',
+ before: {
+ destroy: 'destroyRouterable'
+ }
+ },
+ config: {
+ /**
+ * @cfg {Object} routes
+ * @accessor
+ *
+ * An object of routes to handle hash changes. A route can be defined in a simple way:
+ *
+ * routes: {
+ * 'foo/bar': 'handleFoo',
+ * 'user/:id': 'showUser'
+ * }
+ *
+ * Where the property is the hash (which can accept a parameter defined by a colon)
+ * and the value is the method on the controller to execute. The parameters will get sent
+ * in the action method.
+ *
+ * If no routes match a given hash, an {@link Ext.GlobalEvents#unmatchedroute} event
+ * will be fired. This can be listened to in four ways:
+ *
+ * Ext.on('unmatchedroute', function(token) {});
+ *
+ * Ext.define('MyApp.controller.Foo', {
+ * extend: 'Ext.app.Controller',
+ *
+ * listen: {
+ * global: {
+ * unmatchedroute: 'onUnmatchedRoute'
+ * }
+ * },
+ *
+ * onUnmatchedRoute: function(token) {}
+ * });
+ *
+ * Ext.application({
+ * name: 'MyApp',
+ *
+ * listen: {
+ * global: {
+ * unmatchedroute: 'onUnmatchedRoute'
+ * }
+ * },
+ *
+ * onUnmatchedRoute: function(token) {}
+ * });
+ *
+ * Ext.application({
+ * name: 'MyApp',
+ *
+ * listeners: {
+ * unmatchedroute: 'onUnmatchedRoute'
+ * },
+ *
+ * onUnmatchedRoute: function(token) {}
+ * });
+ *
+ * There is also a complex means of defining a route where you can use a before action
+ * and even specify your own RegEx for the parameter:
+ *
+ * routes: {
+ * 'foo/bar': {
+ * action: 'handleFoo',
+ * before: 'beforeHandleFoo'
+ * },
+ * 'user/:id': {
+ * action: 'showUser',
+ * before: 'beforeShowUser',
+ * conditions: {
+ * ':id': '([0-9]+)'
+ * }
+ * }
+ * }
+ *
+ * This will only match if the `id` parameter is a number.
+ *
+ * The before action allows you to cancel an action. Every before action will get passed
+ * an `action` argument with a `resume` and `stop` methods as the last argument of the
+ * method and you *MUST* execute either method:
+ *
+ * beforeHandleFoo: function (action) {
+ * // some logic here
+ *
+ * // this will allow the handleFoo action to be executed
+ * action.resume();
+ * },
+ * handleFoo: function () {
+ * // will get executed due to true being passed in callback in beforeHandleFoo
+ * },
+ * beforeShowUser: function (id, action) {
+ * // allows for async process like an Ajax
+ * Ext.Ajax.request({
+ * url: 'foo.php',
+ * success: function () {
+ * // will not allow the showUser method to be executed
+ * // but will continue other queued actions.
+ * action.stop();
+ * },
+ * failure: function () {
+ * // will not allow the showUser method to be executed
+ * // and will not allow other queued actions to be executed.
+ * action.stop(true);
+ * }
+ * });
+ * },
+ * showUser: function (id) {
+ * // will not get executed due to false being passed in callback in beforeShowUser
+ * }
+ *
+ * You **MUST** execute the `{@link Ext.route.Action#resume resume}` or
+ * `{@link Ext.route.Action#stop stop}` method on the `action` argument. Executing
+ * `action.resume();` will continue the action, `action.stop();` will prevent
+ * further execution.
+ *
+ * The default RegEx that will be used is `([%a-zA-Z0-9\\-\\_\\s,]+)` but you can specify
+ * any that may suit what you need to accomplish. An example of an advanced condition
+ * may be to make a parameter optional and case-insensitive:
+ *
+ * routes: {
+ * 'user:id': {
+ * action: 'showUser',
+ * before: 'beforeShowUser',
+ * conditions: {
+ * ':id': '(?:(?:\/){1}([%a-z0-9_,\s\-]+))?'
+ * }
+ * }
+ * }
+ *
+ * Each route can be named; this allows for the route to be looked up by name instead of
+ * url. By default, the route's name will be the url you configure but you can provide
+ * the `{@link Ext.route.Route#name name}` config to override the default:
+ *
+ * routes: {
+ * 'user:id': {
+ * action: 'showUser',
+ * before: 'beforeShowUser',
+ * name: 'user',
+ * conditions: {
+ * ':id': '(?:(?:\/){1}([%a-z0-9_,\s\-]+))?'
+ * }
+ * }
+ * }
+ *
+ * The `user:id` route can not be looked up via the `user` name which is useful when using
+ * `{@link #redirectTo}`.
+ *
+ * A wildcard route can also be defined which works exactly like any other route but will
+ * always execute before any other route. To specify a wildcard route, use the `*`
+ * as the url:
+ *
+ * routes: {
+ * '*': 'onToken'
+ * }
+ *
+ * Since a wildcard route will execute before any other route, it can delay the execution
+ * of other routes allowing for such things like a user session to be retrieved:
+ *
+ * routes: {
+ * '*': {
+ * before: 'onBeforeToken'
+ * }
+ * },
+ *
+ * onBeforeToken: function () {
+ * return Ext.Ajax.request({
+ * url: '/user/session'
+ * });
+ * }
+ *
+ * In the above example, no other route will execute unless that
+ * {@link Ext.Ajax#request request} returns successfully.
+ *
+ * You can also use a wildcard route if you need to defer routes until a store has been
+ * loaded when an application first starts up:
+ *
+ * routes: {
+ * '*': {
+ * before: 'onBeforeToken'
+ * }
+ * },
+ *
+ * onBeforeToken: function (action) {
+ * var store = Ext.getStore('Settings');
+ *
+ * if (store.loaded) {
+ * action.resume();
+ * } else {
+ * store.on('load', action.resume, action, { single: true });
+ * }
+ * }
+ *
+ * The valid options are configurations from {@link Ext.route.Handler} and
+ * {@link Ext.route.Route}.
+ */
+ routes: null
+ },
+ destroyRouterable: function() {
+ Ext.route.Router.disconnect(this);
+ },
+ applyRoutes: function(routes, oldRoutes) {
+ var Router = Ext.route.Router,
+ url;
+ if (routes) {
+ for (url in routes) {
+ routes[url] = Router.connect(url, routes[url], this);
+ }
+ }
+ if (oldRoutes) {
+ for (url in oldRoutes) {
+ Router.disconnect(this, oldRoutes[url]);
+ }
+ }
+ return routes;
+ },
+ /**
+ * Update the hash. By default, it will not execute the routes if the current token and the
+ * token passed are the same.
+ *
+ * @param {String/Number/Object/Ext.data.Model} hash The hash to redirect to. The hash can be
+ * of several values:
+ * - **String** The hash to exactly be set to.
+ * - **Number** If `1` is passed, {@link Ext.util.History#forward forward} function will be
+ * executed. If `-1` is passed, {@link Ext.util.History#bck back} function will be executed.
+ * - **Ext.data.Model** If a model instance is passed, the Model's
+ * {@link Ext.data.Model#toUrl toUrl} function will be executed to convert it into a String
+ * value to set the hash to.
+ * - **Object** An Object can be passed to control individual tokens in the full hash.
+ * The key should be an associated {@link Ext.route.Route Route}'s
+ * {@link Ext.route.Route#name name} and the value should be the value of that token
+ * in the complete hash. For example, if you have two routes configured, each token in the
+ * hash that can be matched for each route can be individually controlled:
+ *
+ * routes: {
+ * 'foo/bar': 'onFooBar',
+ * 'baz/:id': {
+ * action: 'onBaz',
+ * name: 'baz'
+ * }
+ * }
+ *
+ * If you pass in a hash of `#foo/bar|baz/1`, each route will execute in response. If you want
+ * to change only the `baz` route but leave the `foo/bar` route in the hash, you can pass only
+ * the `baz` key in an object:
+ *
+ * this.redirectTo({
+ * baz : 'baz/5'
+ * });
+ *
+ * and the resulting hash will be `#foo/bar/|baz/5` and only the `baz` route will execute
+ * in reaction but the `foo/bar` will not react since it's associated token in the hash
+ * remained the same. If you wanted to update the `baz` route and remove `foo/bar`
+ * from the hash, you can set the value to `null`:
+ *
+ * this.redirectTo({
+ * 'foo/bar': null,
+ * baz: 'baz/3'
+ * });
+ *
+ * and the resulting hash will be `#baz/3`. Like before, the `baz` route will execute
+ * in reaction.
+ *
+ * @param {Object} opt An optional `Object` describing how to enact the hash being passed in.
+ * Valid options are:
+ *
+ * - `force` Even if the hash will not change, setting this to `true` will force the
+ * {@link Ext.route.Router Router} to react.
+ * - `replace` When set to `true`, this will replace the current resource in the history stack
+ * with the hash being set.
+ *
+ * For backwards compatibility, if `true` is passed instead of an `Object`, this will set
+ * the `force` option to `true`.
+ *
+ * @return {Boolean} Will return `true` if the token was updated.
+ */
+ redirectTo: function(hash, opt) {
+ var currentHash = Ext.util.History.getToken(),
+ Router = Ext.route.Router,
+ delimiter = Router.getMultipleToken(),
+ tokens = currentHash ? currentHash.split(delimiter) : [],
+ length = tokens.length,
+ force, i, name, obj, route, token, match;
+ if (hash === -1) {
+ return Ext.util.History.back();
+ } else if (hash === 1) {
+ return Ext.util.History.forward();
+ } else if (hash.isModel) {
+ hash = hash.toUrl();
+ } else if (Ext.isObject(hash)) {
+ // Passing an object attempts to replace a token in the hash.
+ for (name in hash) {
+ obj = hash[name];
+ if (!Ext.isObject(obj)) {
+ obj = {
+ token: obj
+ };
+ }
+ if (length) {
+ route = Router.getByName(name);
+ if (route) {
+ match = false;
+ for (i = 0; i < length; i++) {
+ token = tokens[i];
+ if (route.matcherRegex.test(token)) {
+ match = true;
+ if (obj.token) {
+ // a token was found in the hash, replace it
+ if (obj.fn && obj.fn.call(this, token, tokens, obj) === false) {
+ // if the fn returned false, skip update
+
+ continue;
+ }
+ tokens[i] = obj.token;
+ if (obj.force) {
+ // clear lastToken to force recognition
+ route.lastToken = null;
+ }
+ } else {
+ // remove token
+ tokens.splice(i, 1);
+ i--;
+ length--;
+ // reset lastToken
+ route.lastToken = null;
+ }
+ }
+ }
+ if (obj && obj.token && !match) {
+ // a token was not found in the hash, push to the end
+ tokens.push(obj.token);
+ }
+ }
+ } else if (obj && obj.token) {
+ // there is no current hash, push to the end
+ tokens.push(obj.token);
+ }
+ }
+ hash = tokens.join(delimiter);
+ }
+ if (opt === true) {
+ // for backwards compatibility
+ force = opt;
+ opt = null;
+ } else if (opt) {
+ force = opt.force;
+ }
+ length = tokens.length;
+ if (force && length) {
+ for (i = 0; i < length; i++) {
+ token = tokens[i];
+ Router.clearLastTokens(token);
+ }
+ }
+ if (currentHash === hash) {
+ if (force) {
+ // hash won't change, trigger handling anyway
+ Router.onStateChange(hash);
+ }
+ // hash isn't going to change, return false
+ return false;
+ }
+ if (opt && opt.replace) {
+ Ext.util.History.replace(hash);
+ } else {
+ Ext.util.History.add(hash);
+ }
+ return true;
+ },
+ privates: {
+ afterClassMixedIn: function(targetClass) {
+ var proto = targetClass.prototype,
+ routes = proto.routes;
+ if (routes) {
+ delete proto.routes;
+ targetClass.getConfigurator().add({
+ routes: routes
+ });
+ }
+ }
+ }
+});
+
+/**
+ * @protected
+ * @class Ext.app.BaseController
+ * Base class for Controllers.
+ *
+ */
+Ext.define('Ext.app.BaseController', {
+ mixins: [
+ Ext.mixin.Observable,
+ Ext.route.Mixin
+ ],
+ isController: true,
+ config: {
+ /**
+ * @cfg {String} id The id of this controller. You can use this id when dispatching.
+ *
+ * For an example of dispatching, see the examples under the
+ * {@link Ext.app.Controller#cfg-listen listen} config.
+ *
+ * If an id is not explicitly set, it will default to the controller's full classname.
+ *
+ * @accessor
+ */
+ id: undefined,
+ /**
+ * @cfg {Object} control
+ * @accessor
+ *
+ * Adds listeners to components selected via {@link Ext.ComponentQuery}. Accepts an
+ * object containing component paths mapped to a hash of listener functions.
+ * The function value may also be a string matching the name of a method on the
+ * controller.
+ *
+ * In the following example the `updateUser` function is mapped to to the `click`
+ * event on a button component, which is a child of the `useredit` component.
+ *
+ * Ext.define('MyApp.controller.Users', {
+ * extend: 'Ext.app.Controller',
+ *
+ * control: {
+ * 'useredit button[action=save]': {
+ * click: 'updateUser'
+ * }
+ * },
+ *
+ * updateUser: function(button) {
+ * console.log('clicked the Save button');
+ * }
+ * });
+ *
+ * The method you pass to the listener will automatically be resolved on the controller.
+ * In this case, the `updateUser` method that will get executed on the `button` `click`
+ * event will resolve to the `updateUser` method on the controller,
+ *
+ * See {@link Ext.ComponentQuery} for more information on component selectors.
+ */
+ control: null,
+ /**
+ * @cfg {Object} listen
+ * @accessor
+ *
+ * Adds listeners to different event sources (also called "event domains"). The
+ * primary event domain is that of components, but there are also other event domains:
+ * {@link Ext.app.domain.Global Global} domain that intercepts events fired from
+ * {@link Ext.GlobalEvents} Observable instance,
+ * {@link Ext.app.domain.Controller Controller} domain can be used to listen to events
+ * fired by other Controllers, {@link Ext.app.domain.Store Store} domain gives access to
+ * Store events, and {@link Ext.app.domain.Direct Direct} domain can be used with
+ * Ext Direct Providers to listen to their events.
+ *
+ * To listen to "bar" events fired by a controller with id="foo":
+ *
+ * Ext.define('AM.controller.Users', {
+ * extend: 'Ext.app.Controller',
+ *
+ * listen: {
+ * controller: {
+ * '#foo': {
+ * bar: 'onFooBar'
+ * }
+ * }
+ * }
+ * });
+ *
+ * To listen to "bar" events fired by any controller, and "baz" events
+ * fired by Store with storeId="baz":
+ *
+ * Ext.define('AM.controller.Users', {
+ * extend: 'Ext.app.Controller',
+ *
+ * listen: {
+ * controller: {
+ * '*': {
+ * bar: 'onAnyControllerBar'
+ * }
+ * },
+ * store: {
+ * '#baz': {
+ * baz: 'onStoreBaz'
+ * }
+ * }
+ * }
+ * });
+ *
+ * To listen to "idle" events fired by {@link Ext.GlobalEvents} when other event
+ * processing is complete and Ext JS is about to return control to the browser:
+ *
+ * Ext.define('AM.controller.Users', {
+ * extend: 'Ext.app.Controller',
+ *
+ * listen: {
+ * global: { // Global events are always fired
+ * idle: 'onIdle' // from the same object, so there
+ * } // are no selectors
+ * }
+ * });
+ *
+ * As this relates to components, the following example:
+ *
+ * Ext.define('AM.controller.Users', {
+ * extend: 'Ext.app.Controller',
+ *
+ * listen: {
+ * component: {
+ * 'useredit button[action=save]': {
+ * click: 'updateUser'
+ * }
+ * }
+ * }
+ * });
+ *
+ * Is equivalent to:
+ *
+ * Ext.define('AM.controller.Users', {
+ * extend: 'Ext.app.Controller',
+ *
+ * control: {
+ * 'useredit button[action=save]': {
+ * click: 'updateUser'
+ * }
+ * }
+ * });
+ *
+ * Of course, these can all be combined in a single call and used instead of
+ * `control`, like so:
+ *
+ * Ext.define('AM.controller.Users', {
+ * extend: 'Ext.app.Controller',
+ *
+ * listen: {
+ * global: {
+ * idle: 'onIdle'
+ * },
+ * controller: {
+ * '*': {
+ * foobar: 'onAnyFooBar'
+ * },
+ * '#foo': {
+ * bar: 'onFooBar'
+ * }
+ * },
+ * component: {
+ * 'useredit button[action=save]': {
+ * click: 'updateUser'
+ * }
+ * },
+ * store: {
+ * '#qux': {
+ * load: 'onQuxLoad'
+ * }
+ * }
+ * }
+ * });
+ */
+ listen: null
+ },
+ /**
+ * Creates new Controller.
+ *
+ * @param {Object} [config] Configuration object.
+ */
+ constructor: function(config) {
+ var me = this;
+ // In versions prior to 5.1, this constructor used to call the Ext.util.Observable
+ // constructor (which applied the config properties directly to the instance)
+ // AND it used to call initConfig as well. Since the constructor of
+ // Ext.mixin.Observable calls initConfig, but does not apply the properties to
+ // the instance, we do that here for backward compatibility.
+ Ext.apply(me, config);
+ // The control and listen properties are also methods so we need to delete them
+ // from the instance after applying the config object.
+ delete me.control;
+ delete me.listen;
+ me.eventbus = Ext.app.EventBus;
+ // need to have eventbus property set before we initialize the config
+ me.mixins.observable.constructor.call(me, config);
+ },
+ updateId: function(id) {
+ this.id = id;
+ },
+ applyListen: function(listen) {
+ if (Ext.isObject(listen)) {
+ listen = Ext.clone(listen);
+ }
+ return listen;
+ },
+ applyControl: function(control) {
+ if (Ext.isObject(control)) {
+ control = Ext.clone(control);
+ }
+ return control;
+ },
+ /**
+ * @param {Object} control The object to pass to the {@link #method-control} method
+ * @private
+ */
+ updateControl: function(control) {
+ this.getId();
+ if (control) {
+ this.control(control);
+ }
+ },
+ /**
+ * @param {Object} listen The object to pass to the {@link #method-listen} method
+ * @private
+ */
+ updateListen: function(listen) {
+ this.getId();
+ if (listen) {
+ this.listen(listen);
+ }
+ },
+ isActive: function() {
+ return true;
+ },
+ /**
+ * Adds listeners to components selected via {@link Ext.ComponentQuery}. Accepts an
+ * object containing component paths mapped to a hash of listener functions.
+ *
+ * In the following example the `updateUser` function is mapped to to the `click`
+ * event on a button component, which is a child of the `useredit` component.
+ *
+ * Ext.define('AM.controller.Users', {
+ * init: function() {
+ * this.control({
+ * 'useredit button[action=save]': {
+ * click: this.updateUser
+ * }
+ * });
+ * },
+ *
+ * updateUser: function(button) {
+ * console.log('clicked the Save button');
+ * }
+ * });
+ *
+ * Or alternatively one call `control` with two arguments:
+ *
+ * this.control('useredit button[action=save]', {
+ * click: this.updateUser
+ * });
+ *
+ * See {@link Ext.ComponentQuery} for more information on component selectors.
+ *
+ * @param {String/Object} selectors If a String, the second argument is used as the
+ * listeners, otherwise an object of selectors -> listeners is assumed
+ * @param {Object} [listeners] Config for listeners.
+ * @param {Ext.app.BaseController} [controller] (private)
+ */
+ control: function(selectors, listeners, controller) {
+ var me = this,
+ ctrl = controller,
+ obj;
+ if (Ext.isString(selectors)) {
+ obj = {};
+ obj[selectors] = listeners;
+ } else {
+ obj = selectors;
+ ctrl = listeners;
+ }
+ me.eventbus.control(obj, ctrl || me);
+ },
+ /**
+ * Adds listeners to different event sources (also called "event domains"). The
+ * primary event domain is that of components, but there are also other event domains:
+ * {@link Ext.app.domain.Global Global} domain that intercepts events fired from
+ * {@link Ext.GlobalEvents} Observable instance, {@link Ext.app.domain.Controller Controller}
+ * domain can be used to listen to events fired by other Controllers,
+ * {@link Ext.app.domain.Store Store} domain gives access to Store events, and
+ * {@link Ext.app.domain.Direct Direct} domain can be used with Ext Direct Providers
+ * to listen to their events.
+ *
+ * To listen to "bar" events fired by a controller with id="foo":
+ *
+ * Ext.define('AM.controller.Users', {
+ * init: function() {
+ * this.listen({
+ * controller: {
+ * '#foo': {
+ * bar: this.onFooBar
+ * }
+ * }
+ * });
+ * },
+ * ...
+ * });
+ *
+ * To listen to "bar" events fired by any controller, and "baz" events
+ * fired by Store with storeId="baz":
+ *
+ * Ext.define('AM.controller.Users', {
+ * init: function() {
+ * this.listen({
+ * controller: {
+ * '*': {
+ * bar: this.onAnyControllerBar
+ * }
+ * },
+ * store: {
+ * '#baz': {
+ * baz: this.onStoreBaz
+ * }
+ * }
+ * });
+ * },
+ * ...
+ * });
+ *
+ * To listen to "idle" events fired by {@link Ext.GlobalEvents} when other event
+ * processing is complete and Ext JS is about to return control to the browser:
+ *
+ * Ext.define('AM.controller.Users', {
+ * init: function() {
+ * this.listen({
+ * global: { // Global events are always fired
+ * idle: this.onIdle // from the same object, so there
+ * } // are no selectors
+ * });
+ * }
+ * });
+ *
+ * As this relates to components, the following example:
+ *
+ * Ext.define('AM.controller.Users', {
+ * init: function() {
+ * this.listen({
+ * component: {
+ * 'useredit button[action=save]': {
+ * click: this.updateUser
+ * }
+ * }
+ * });
+ * },
+ * ...
+ * });
+ *
+ * Is equivalent to:
+ *
+ * Ext.define('AM.controller.Users', {
+ * init: function() {
+ * this.control({
+ * 'useredit button[action=save]': {
+ * click: this.updateUser
+ * }
+ * });
+ * },
+ * ...
+ * });
+ *
+ * Of course, these can all be combined in a single call and used instead of
+ * `control`, like so:
+ *
+ * Ext.define('AM.controller.Users', {
+ * init: function() {
+ * this.listen({
+ * global: {
+ * idle: this.onIdle
+ * },
+ * controller: {
+ * '*': {
+ * foobar: this.onAnyFooBar
+ * },
+ * '#foo': {
+ * bar: this.onFooBar
+ * }
+ * },
+ * component: {
+ * 'useredit button[action=save]': {
+ * click: this.updateUser
+ * }
+ * },
+ * store: {
+ * '#qux': {
+ * load: this.onQuxLoad
+ * }
+ * }
+ * });
+ * },
+ * ...
+ * });
+ *
+ * @param {Object} to Config object containing domains, selectors and listeners.
+ * @param {Ext.app.Controller} [controller] The controller to add the listeners to. Defaults
+ * to the current controller.
+ */
+ listen: function(to, controller) {
+ this.eventbus.listen(to, controller || this);
+ },
+ destroy: function() {
+ var me = this,
+ bus = me.eventbus;
+ if (bus) {
+ bus.unlisten(me);
+ me.eventbus = null;
+ }
+ me.callParent();
+ }
+});
+
+/**
+ * @private
+ * @class Ext.app.Util
+ */
+Ext.define('Ext.app.Util', {}, function() {
+ Ext.apply(Ext.app, {
+ namespaces: {
+ Ext: {}
+ },
+ /**
+ * Adds namespace(s) to known list.
+ * @private
+ *
+ * @param {String/String[]} namespace
+ */
+ addNamespaces: function(namespace) {
+ var namespaces = Ext.app.namespaces,
+ i, l;
+ if (!Ext.isArray(namespace)) {
+ namespace = [
+ namespace
+ ];
+ }
+ for (i = 0 , l = namespace.length; i < l; i++) {
+ namespaces[namespace[i]] = true;
+ }
+ },
+ /**
+ * Clear all namespaces from known list.
+ * @private
+ */
+ clearNamespaces: function() {
+ Ext.app.namespaces = {};
+ },
+ /**
+ * Get namespace prefix for a class name.
+ * @private
+ * @param {String} className
+ *
+ * @return {String} Namespace prefix if it's known, otherwise undefined
+ */
+ getNamespace: function(className) {
+ var namespaces = Ext.apply({}, Ext.ClassManager.paths, Ext.app.namespaces),
+ deepestPrefix = '',
+ prefix;
+ for (prefix in namespaces) {
+ if (namespaces.hasOwnProperty(prefix) && prefix.length > deepestPrefix.length && (prefix + '.' === className.substring(0, prefix.length + 1))) {
+ deepestPrefix = prefix;
+ }
+ }
+ return deepestPrefix === '' ? undefined : deepestPrefix;
+ },
+ /**
+ * Sets up paths based on the `appFolder` and `paths` configs.
+ * @param {String} appName The application name (root namespace).
+ * @param {String} appFolder The folder for app sources ("app" by default).
+ * @param {Object} paths A set of namespace to path mappings.
+ * @private
+ * @since 6.0.0
+ */
+ setupPaths: function(appName, appFolder, paths) {
+ var manifestPaths = Ext.manifest,
+ ns;
+ // Ignore appFolder:null
+ if (appName && appFolder !== null) {
+ manifestPaths = manifestPaths && manifestPaths.paths;
+ // If the manifest has paths, only honor appFolder if defined. If the
+ // manifest has no paths (old school mode), then we want to default an
+ // unspecified appFolder value to "app". Sencha Cmd will pass in paths
+ // to configure the loader via the "paths" property of the manifest so
+ // we don't want to try and be "helpful" in that case.
+ if (!manifestPaths || appFolder !== undefined) {
+ Ext.Loader.setPath(appName, (appFolder === undefined) ? 'app' : appFolder);
+ }
+ }
+ if (paths) {
+ for (ns in paths) {
+ if (paths.hasOwnProperty(ns)) {
+ Ext.Loader.setPath(ns, paths[ns]);
+ }
+ }
+ }
+ }
+ });
+ /**
+ * @method getNamespace
+ * @member Ext
+ * @param {String} className
+ *
+ * @return {String} Namespace prefix if it's known, otherwise undefined
+ */
+ Ext.getNamespace = Ext.app.getNamespace;
+});
+
+/**
+ * @private
+ */
+Ext.define('Ext.util.BasicFilter', {
+ isFilter: true,
+ config: {
+ /**
+ * @cfg {String} id
+ * An identifier by which this Filter is known, for example, as a member of a
+ * {@link Ext.data.Store#cfg-filters Store's filters collection}.
+ *
+ * Identified filters are manageable in such collections because they can be found
+ * or removed using their `id`.
+ */
+ id: null,
+ /**
+ * @cfg {Boolean} disabled
+ * Setting this property to `true` disables this individual filter.
+ */
+ disabled: false,
+ /**
+ * @cfg {Function} serializer
+ * A function to post-process any serialization. Accepts the serialized filter
+ * containing `property`, `value` and `operator` properties, and may either
+ * mutate it, or return a completely new representation. Returning a falsy
+ * value does not modify the representation.
+ * @since 6.2.0
+ */
+ serializer: null
+ },
+ /**
+ * @property {Number} generation
+ * this property is a Mutation counter which is incremented whenever the filter changes
+ * in a way that may change either its serialized form or its result.
+ * @readonly
+ * @since 6.5.0
+ */
+ generation: 0,
+ /**
+ * Initializes a filter.
+ * @param {Object} config The config object
+ */
+ constructor: function(config) {
+ this.initConfig(config);
+ },
+ updateDisabled: function() {
+ // Developers may use this to see if a filter has changed in ways that must cause
+ // a reevaluation of filtering
+ if (!this.isConfiguring) {
+ ++this.generation;
+ }
+ }
+});
+/**
+ * @method filter
+ * @param {Object} item
+ * @return {Boolean}
+ */
+/**
+ * @method serialize
+ * Returns this filter's serialized state. This is used when transmitting this filter
+ * to a server.
+ * @return {Object}
+ */
+/**
+ * Serialize this filter into the `out` array (if possible).
+ * @param {Array} out The array of simple and-able filters.
+ * @return {Boolean} `true` if any saved filters require encoding
+ * @method serializeTo
+ * @private
+ */
+
+/**
+ * A filter that can be applied to an `Ext.util.Collection` or other data container such
+ * an `Ext.data.Store`. A `Filter` can be simply a filter on a `property` and `value` pair
+ * or a filter function with custom logic.
+ *
+ * Normally filters are added to stores or collections but they can be created directly:
+ *
+ * var ageFilter = new Ext.util.Filter({
+ * property: 'age',
+ * value: 42,
+ * operator: '<'
+ * });
+ *
+ * var longNameFilter = new Ext.util.Filter({
+ * filterFn: function(item) {
+ * return item.name.length > 4;
+ * }
+ * });
+ */
+Ext.define('Ext.util.Filter', {
+ extend: Ext.util.BasicFilter,
+ config: {
+ /**
+ * @cfg {String} property
+ * The property to filter on. Required unless a {@link #filterFn} is passed.
+ */
+ property: null,
+ /**
+ * @cfg {String} root
+ * This property is used to descend items to check for meaningful properties on
+ * which to filter. For a `Ext.data.Model` for example this would be `'data'`.
+ */
+ root: null,
+ /**
+ * @cfg {RegExp/Mixed} value
+ * The value you want to match against. Required unless a {@link #filterFn} is passed.
+ *
+ * Can be a regular expression which will be used as a matcher or any other value
+ * such as an object or an array of objects. This value is compared using the configured
+ * {@link #operator}.
+ */
+ value: null,
+ /**
+ * @cfg {Function} filterFn
+ * A custom filter function which is passed each item. This function must return
+ * `true` to accept an item or `false` to reject it.
+ */
+ filterFn: null,
+ /**
+ * @cfg {Boolean} anyMatch
+ * True to allow any match - no regex start/end line anchors will be added.
+ */
+ anyMatch: false,
+ /**
+ * @cfg {Boolean} exactMatch
+ * True to force exact match (^ and $ characters added to the regex). Ignored if
+ * `anyMatch` is `true`.
+ */
+ exactMatch: false,
+ /**
+ * @cfg {Boolean} caseSensitive
+ * True to make the regex case sensitive (adds 'i' switch to regex).
+ */
+ caseSensitive: false,
+ /**
+ * @cfg {Boolean} disableOnEmpty
+ * `true` to not have this filter participate in the filtering process when the
+ * {@link #value} of this the filter is empty according to {@link Ext#isEmpty}.
+ *
+ * @since 5.1.0
+ */
+ disableOnEmpty: false,
+ /**
+ * @cfg {String} operator
+ * The operator to use to compare the {@link #cfg!property} to this Filter's
+ * {@link #cfg!value}.
+ *
+ * Possible values are:
+ *
+ * * `<`
+ * * `<=`
+ * * `=`
+ * * `>=`
+ * * `>`
+ * * `!=`
+ * * `in`
+ * * `notin`
+ * * `like`
+ * * `/=`
+ *
+ * The `in` and `notin` operator expects this filter's {@link #cfg-value} to be
+ * an array and matches values that are present in that array.
+ *
+ * The `like` operator matches values that contain this filter's {@link #cfg-value}
+ * as a substring.
+ *
+ * The `/=` operator uses the {@link #cfg-value} as the source for a `RegExp` and
+ * tests whether the candidate value matches the regular expression.
+ */
+ operator: null,
+ /**
+ * @cfg {Function} [convert]
+ * A function to do any conversion on the value before comparison. For example,
+ * something that returns the date only part of a date.
+ * @cfg {Object} convert.value The value to convert.
+ * @cfg {Object} convert.return The converted value.
+ * @private
+ */
+ convert: null
+ },
+ /**
+ * @cfg {Object} [scope]
+ * The context (`this` property) in which the filtering function is called. Defaults
+ * to this Filter object.
+ */
+ scope: null,
+ // Needed for scope above. If `scope` were a "config" it would be merged and lose its
+ // identity.
+ $configStrict: false,
+ statics: {
+ /**
+ * Creates a single filter function which encapsulates the passed Filter array or
+ * Collection.
+ * @param {Ext.util.Filter[]/Ext.util.Collection} filters The filters from which to
+ * create a filter function.
+ * @return {Function} A function, which when passed a candidate object returns `true`
+ * if the candidate passes all the specified Filters.
+ */
+ createFilterFn: function(filters) {
+ if (!filters) {
+ return Ext.returnTrue;
+ }
+ return function(candidate) {
+ var items = filters.isCollection ? filters.items : filters,
+ length = items.length,
+ match = true,
+ i, filter;
+ for (i = 0; match && i < length; i++) {
+ filter = items[i];
+ // Skip disabled filters
+ if (!filter.getDisabled()) {
+ match = filter.filter(candidate);
+ }
+ }
+ return match;
+ };
+ },
+ /**
+ * Checks if two filters have the same properties (Property, Operator and Value).
+ *
+ * @param {Ext.util.Filter} filter1 The first filter to be compared
+ * @param {Ext.util.Filter} filter2 The second filter to be compared
+ * @return {Boolean} `true` if they have the same properties.
+ * @since 6.2.0
+ */
+ isEqual: function(filter1, filter2) {
+ if (filter1.getProperty() !== filter2.getProperty()) {
+ return false;
+ }
+ if (filter1.getOperator() !== filter2.getOperator()) {
+ return false;
+ }
+ if (filter1.getValue() === filter2.getValue()) {
+ return true;
+ }
+ if (Ext.isArray(filter1) && Ext.isArray(filter2)) {
+ return Ext.Array.equals(filter1, filter2);
+ }
+ return false;
+ },
+ /**
+ * Checks whether the filter will produce a meaningful value. Since filters
+ * may be used in conjunction with data binding, this is a sanity check to
+ * check whether the resulting filter will be able to match.
+ *
+ * @param {Object} cfg The filter config object
+ * @return {Boolean/String} `true` if the filter will produce a valid value
+ *
+ * @private
+ */
+ isInvalid: function(cfg) {
+ if (!cfg.filterFn) {
+ // If we don't have a filterFn, we must have a property
+ if (!cfg.property) {
+ return 'A Filter requires either a property or a filterFn to be set';
+ }
+ if (!cfg.hasOwnProperty('value') && !cfg.operator) {
+ return 'A Filter requires either a property and value, or a filterFn to be set';
+ }
+ }
+ return false;
+ }
+ },
+ constructor: function(config) {
+ var warn = Ext.util.Filter.isInvalid(config);
+ if (warn) {
+ Ext.log.warn(warn);
+ }
+ this.callParent([
+ config
+ ]);
+ },
+ preventConvert: {
+ 'in': 1,
+ notin: 1
+ },
+ filter: function(item) {
+ var me = this,
+ filterFn = me._filterFn || me.getFilterFn(),
+ convert = me.getConvert(),
+ value = me._value;
+ me._filterValue = value;
+ me.isDateValue = Ext.isDate(value);
+ if (me.isDateValue) {
+ me.dateValue = value.getTime();
+ }
+ if (convert && !me.preventConvert[me.getOperator()]) {
+ me._filterValue = convert.call(me.scope || me, value);
+ }
+ return filterFn.call(me.scope || me, item);
+ },
+ getId: function() {
+ var me = this,
+ id = me._id;
+ if (!id) {
+ id = me.getProperty();
+ if (!id) {
+ id = Ext.id(null, 'ext-filter-');
+ }
+ me._id = id;
+ }
+ return id;
+ },
+ getFilterFn: function() {
+ var me = this,
+ filterFn = me._filterFn,
+ operator;
+ if (!filterFn) {
+ operator = me.getOperator();
+ if (operator) {
+ filterFn = me.operatorFns[operator];
+ } else {
+ // This part is broken our into its own method so the function expression
+ // contained there does not get hoisted and created on each call this
+ // method.
+ filterFn = me.createRegexFilter();
+ }
+ me._filterFn = filterFn;
+ // Mark as generated by default. This becomes important when proxies encode
+ // filters. See proxy.Server#encodeFilters().
+ me.generatedFilterFn = true;
+ }
+ return filterFn;
+ },
+ /**
+ * @private
+ * Creates a filter function for the configured value/anyMatch/caseSensitive options
+ * for this Filter.
+ */
+ createRegexFilter: function() {
+ var me = this,
+ anyMatch = !!me.getAnyMatch(),
+ exact = !!me.getExactMatch(),
+ value = me.getValue(),
+ matcher = Ext.String.createRegex(value, !anyMatch, // startsWith
+ !anyMatch && exact, // endsWith
+ !me.getCaseSensitive());
+ return function(item) {
+ var val = me.getPropertyValue(item);
+ return matcher ? matcher.test(val) : (val == null);
+ };
+ },
+ /**
+ * Returns the property of interest from the given item, based on the configured `root`
+ * and `property` configs.
+ * @param {Object} item The item.
+ * @return {Object} The property of the object.
+ * @private
+ */
+ getPropertyValue: function(item) {
+ var root = this._root,
+ value = (root == null) ? item : item[root];
+ return value[this._property];
+ },
+ /**
+ * Returns this filter's state.
+ * @return {Object}
+ */
+ getState: function() {
+ var config = this.getInitialConfig(),
+ result = {},
+ name;
+ for (name in config) {
+ // We only want the instance properties in this case, not inherited ones,
+ // so we need hasOwnProperty to filter out our class values.
+ if (config.hasOwnProperty(name)) {
+ result[name] = config[name];
+ }
+ }
+ delete result.root;
+ result.value = this.getValue();
+ return result;
+ },
+ getScope: function() {
+ return this.scope;
+ },
+ /**
+ * Returns this filter's serialized state. This is used when transmitting this filter
+ * to a server.
+ * @return {Object}
+ */
+ serialize: function() {
+ var result = this.getState(),
+ serializer = this.getSerializer(),
+ serialized;
+ delete result.id;
+ delete result.serializer;
+ if (serializer) {
+ serialized = serializer.call(this, result);
+ if (serialized) {
+ result = serialized;
+ }
+ }
+ return result;
+ },
+ serializeTo: function(out) {
+ var me = this,
+ primitive, serialized;
+ // Filters with a custom filterFn cannot be serialized. But since #getFilterFn()
+ // always returns a filterFn, we need to check if it's been generated by default.
+ // If so, we know that the filter cannot have a custom filterFn defined, and it
+ // is therefore okay to serialize.
+ me.getFilterFn();
+ if (me.generatedFilterFn) {
+ out.push(serialized = me.serialize());
+ primitive = me.primitiveRe.test(typeof serialized);
+ }
+ return !primitive;
+ },
+ updateOperator: function() {
+ // Need to clear any generated local filter fn and increment generation
+ this.onConfigMutation();
+ },
+ updateConvert: function() {
+ // Need to clear any generated local filter fn and increment generation
+ this.onConfigMutation();
+ },
+ updateProperty: function() {
+ // Need to clear any generated local filter fn and increment generation
+ this.onConfigMutation();
+ },
+ updateAnyMatch: function() {
+ // Need to clear any generated local filter fn and increment generation
+ this.onConfigMutation();
+ },
+ updateExactMatch: function() {
+ // Need to clear any generated local filter fn and increment generation
+ this.onConfigMutation();
+ },
+ updateCaseSensitive: function() {
+ // Need to clear any generated local filter fn and increment generation
+ this.onConfigMutation();
+ },
+ updateValue: function(value) {
+ // Need to clear any generated local filter fn and increment generation
+ this.onConfigMutation();
+ if (this.getDisableOnEmpty()) {
+ this.setDisabled(Ext.isEmpty(value));
+ }
+ },
+ updateFilterFn: function(filterFn) {
+ delete this.generatedFilterFn;
+ },
+ onConfigMutation: function() {
+ // Developers may use this to see if a filter has changed in ways that must cause
+ // a reevaluation of filtering
+ this.generation++;
+ if (this.generatedFilterFn) {
+ this._filterFn = null;
+ }
+ },
+ updateDisableOnEmpty: function(disableOnEmpty) {
+ // Only poke disabled if true because otherwise we'll smash the disabled
+ // config that may also be getting set.
+ if (disableOnEmpty) {
+ this.setDisabled(Ext.isEmpty(this.getValue()));
+ }
+ },
+ privates: {
+ primitiveRe: /string|number|boolean/,
+ getCandidateValue: function(candidate, v, preventCoerce) {
+ var me = this,
+ convert = me._convert,
+ result = me.getPropertyValue(candidate);
+ if (convert) {
+ result = convert.call(me.scope || me, result);
+ } else if (!preventCoerce) {
+ result = Ext.coerce(result, v);
+ }
+ return result;
+ }
+ }
+}, function(Filter) {
+ var prototype = Filter.prototype,
+ operatorFns = (prototype.operatorFns = {
+ "<": function(candidate) {
+ var v = this._filterValue;
+ return this.getCandidateValue(candidate, v) < v;
+ },
+ "<=": function(candidate) {
+ var v = this._filterValue;
+ return this.getCandidateValue(candidate, v) <= v;
+ },
+ "=": function(candidate) {
+ var me = this,
+ v = me._filterValue;
+ candidate = me.getCandidateValue(candidate, v);
+ if (me.isDateValue && candidate instanceof Date) {
+ candidate = candidate.getTime();
+ v = me.dateValue;
+ }
+ return candidate == v;
+ },
+ // eslint-disable-line eqeqeq
+ "===": function(candidate) {
+ var me = this,
+ v = me._filterValue;
+ candidate = me.getCandidateValue(candidate, v, true);
+ if (me.isDateValue && candidate instanceof Date) {
+ candidate = candidate.getTime();
+ v = me.dateValue;
+ }
+ return candidate === v;
+ },
+ ">=": function(candidate) {
+ var v = this._filterValue;
+ return this.getCandidateValue(candidate, v) >= v;
+ },
+ ">": function(candidate) {
+ var v = this._filterValue;
+ return this.getCandidateValue(candidate, v) > v;
+ },
+ "!=": function(candidate) {
+ var me = this,
+ v = me._filterValue;
+ candidate = me.getCandidateValue(candidate, v);
+ if (me.isDateValue && candidate instanceof Date) {
+ candidate = candidate.getTime();
+ v = me.dateValue;
+ }
+ return candidate != v;
+ },
+ // eslint-disable-line eqeqeq
+ "!==": function(candidate) {
+ var me = this,
+ v = me._filterValue;
+ candidate = me.getCandidateValue(candidate, v, true);
+ if (me.isDateValue && candidate instanceof Date) {
+ candidate = candidate.getTime();
+ v = me.dateValue;
+ }
+ return candidate !== v;
+ },
+ "in": function(candidate) {
+ var v = this._filterValue;
+ return Ext.Array.contains(v, this.getCandidateValue(candidate, v));
+ },
+ notin: function(candidate) {
+ var v = this._filterValue;
+ return !Ext.Array.contains(v, this.getCandidateValue(candidate, v));
+ },
+ like: function(candidate) {
+ var v = this._filterValue;
+ // eslint-disable-next-line max-len
+ return v && this.getCandidateValue(candidate, v).toLowerCase().indexOf(v.toLowerCase()) > -1;
+ },
+ "/=": function(candidate) {
+ var me = this,
+ v = me._filterValue;
+ candidate = me.getCandidateValue(candidate, v);
+ // Only compile a RegExp when the source string changes
+ if (v !== me.lastRegExpSource) {
+ me.lastRegExpSource = v;
+ try {
+ me.regex = new RegExp(v, 'i');
+ } catch (e) {
+ me.regex = null;
+ }
+ }
+ return me.regex ? me.regex.test(candidate) : false;
+ }
+ });
+ // Operator type '==' is the same as operator type '='
+ operatorFns['=='] = operatorFns['='];
+ operatorFns.gt = operatorFns['>'];
+ operatorFns.ge = operatorFns['>='];
+ operatorFns.lt = operatorFns['<'];
+ operatorFns.le = operatorFns['<='];
+ operatorFns.eq = operatorFns['='];
+ operatorFns.ne = operatorFns['!='];
+});
+
+/**
+ * @class Ext.util.AbstractMixedCollection
+ * @private
+ */
+Ext.define('Ext.util.AbstractMixedCollection', {
+ mixins: {
+ observable: Ext.util.Observable
+ },
+ /**
+ * @property {Boolean} isMixedCollection
+ * `true` in this class to identify an object as an instantiated MixedCollection,
+ * or subclass thereof.
+ */
+ isMixedCollection: true,
+ /**
+ * Mutation counter which is incremented upon add and remove.
+ *
+ * @private
+ */
+ generation: 0,
+ /**
+ * Mutation counter for the index map which is synchronized with the collection's
+ * mutation counter when the index map is interrogated and found to be out of sync
+ * and needed a rebuild.
+ *
+ * @private
+ */
+ indexGeneration: 0,
+ constructor: function(allowFunctions, keyFn) {
+ var me = this;
+ // Modern constructor signature using a config object
+ if (arguments.length === 1 && Ext.isObject(allowFunctions)) {
+ me.initialConfig = allowFunctions;
+ Ext.apply(me, allowFunctions);
+ } else // Old constructor signature
+ {
+ me.allowFunctions = allowFunctions === true;
+ if (keyFn) {
+ me.getKey = keyFn;
+ }
+ me.initialConfig = {
+ allowFunctions: me.allowFunctions,
+ getKey: me.getKey
+ };
+ }
+ me.items = [];
+ me.map = {};
+ me.keys = [];
+ me.indexMap = {};
+ me.length = 0;
+ /**
+ * @event clear
+ * Fires when the collection is cleared.
+ * @since 1.1.0
+ */
+ /**
+ * @event add
+ * Fires when an item is added to the collection.
+ * @param {Number} index The index at which the item was added.
+ * @param {Object} o The item added.
+ * @param {String} key The key associated with the added item.
+ * @since 1.1.0
+ */
+ /**
+ * @event replace
+ * Fires when an item is replaced in the collection.
+ * @param {String} key he key associated with the new added.
+ * @param {Object} old The item being replaced.
+ * @param {Object} new The new item.
+ * @since 1.1.0
+ */
+ /**
+ * @event remove
+ * Fires when an item is removed from the collection.
+ * @param {Object} o The item being removed.
+ * @param {String} key The key associated with the removed item.
+ * @since 1.1.0
+ */
+ me.mixins.observable.constructor.call(me);
+ },
+ destroy: function() {
+ var me = this;
+ me.items = me.map = me.keys = me.indexMap = null;
+ me.callParent();
+ },
+ /**
+ * @cfg {Boolean} allowFunctions Specify true
if the {@link #addAll}
+ * function should add function references to the collection. Defaults to
+ * false
.
+ * @since 3.4.0
+ */
+ allowFunctions: false,
+ /**
+ * Adds an item to the collection. Fires the {@link #event-add} event when complete.
+ *
+ * @param {String/Object} key The key to associate with the item, or the new item.
+ *
+ * If a {@link #getKey} implementation was specified for this MixedCollection,
+ * or if the key of the stored items is in a property called `id`,
+ * the MixedCollection will be able to *derive* the key for the new item.
+ * In this case just pass the new item in this parameter.
+ *
+ * @param {Object} [obj] The item to add.
+ *
+ * Note that when adding a value that is iterable, it must be wrapped in brackets, i.e.:
+ *
+ * c.add([[1, 2]]);
+ *
+ * This will be needed for any value that is iterable, i.e., an array, arguments object,
+ * HTML collections, etc.
+ *
+ * @return {Object} The item added.
+ * @since 1.1.0
+ */
+ add: function(key, obj) {
+ var len = this.length,
+ out;
+ if (arguments.length === 1) {
+ out = this.insert(len, key);
+ } else {
+ out = this.insert(len, key, obj);
+ }
+ return out;
+ },
+ /**
+ * A function which will be called, passing a newly added object
+ * when the object is added without a separate id. The function
+ * should yield the key by which that object will be indexed.
+ *
+ * If no key is yielded, then the object will be added, but it
+ * cannot be accessed or removed quickly. Finding it in this
+ * collection for interrogation or removal will require a linear
+ * scan of this collection's items.
+ *
+ * The default implementation simply returns `item.id` but you can
+ * provide your own implementation to return a different value as
+ * in the following examples:
+ *
+ * // normal way
+ * var mc = new Ext.util.MixedCollection();
+ * mc.add(someEl.dom.id, someEl);
+ * mc.add(otherEl.dom.id, otherEl);
+ * //and so on
+ *
+ * // using getKey
+ * var mc = new Ext.util.MixedCollection({
+ * getKey: function(el){
+ * return el.dom.id;
+ * }
+ * });
+ * mc.add(someEl);
+ * mc.add(otherEl);
+ *
+ * @param {Object} item The item for which to find the key.
+ * @return {Object} The key for the passed item.
+ * @since 1.1.0
+ * @template
+ */
+ getKey: function(item) {
+ return item.id;
+ },
+ /**
+ * Replaces an item in the collection. Fires the {@link #event-replace} event when complete.
+ * @param {String} key The key associated with the item to replace, or the replacement item.
+ *
+ * If you supplied a {@link #getKey} implementation for this MixedCollection, or if the key
+ * of your stored items is in a property called *`id`*, then the MixedCollection
+ * will be able to derive the key of the replacement item. If you want to replace an item
+ * with one having the same key value, then just pass the replacement item in this parameter.
+ *
+ * @param o {Object} o (optional) If the first parameter passed was a key, the item to associate
+ * with that key.
+ * @return {Object} The new item.
+ * @since 1.1.0
+ */
+ replace: function(key, o) {
+ var me = this,
+ old, index;
+ if (arguments.length === 1) {
+ o = arguments[0];
+ key = me.getKey(o);
+ }
+ old = me.map[key];
+ if (typeof key === 'undefined' || key === null || typeof old === 'undefined') {
+ return me.add(key, o);
+ }
+ me.generation++;
+ index = me.indexOfKey(key);
+ me.items[index] = o;
+ me.map[key] = o;
+ if (me.hasListeners.replace) {
+ me.fireEvent('replace', key, old, o);
+ }
+ return o;
+ },
+ /**
+ * Reorders each of the items based on a mapping from old index to new index. Internally this
+ * just translates into a sort. The 'sort' event is fired whenever reordering has
+ * occurred.
+ * @param {Object} mapping Mapping from old item index to new item index
+ *
+ * // example of moving the last of 4 items to the front of the collection
+ * // and moving each one before it forward one
+ * collection.reorder({
+ * 0: 1,
+ * 1: 2,
+ * 2: 3,
+ * 3: 0,
+ * });
+ */
+ reorder: function(mapping) {
+ var me = this,
+ items = me.items,
+ index = 0,
+ length = items.length,
+ order = [],
+ remaining = [],
+ oldIndex;
+ me.suspendEvents();
+ // object of {oldPosition: newPosition} reversed to {newPosition: oldPosition}
+ for (oldIndex in mapping) {
+ order[mapping[oldIndex]] = items[oldIndex];
+ }
+ for (index = 0; index < length; index++) {
+ if (mapping[index] == undefined) {
+ // eslint-disable-line eqeqeq
+ remaining.push(items[index]);
+ }
+ }
+ for (index = 0; index < length; index++) {
+ if (order[index] == undefined) {
+ // eslint-disable-line eqeqeq
+ order[index] = remaining.shift();
+ }
+ }
+ me.clear();
+ me.addAll(order);
+ me.resumeEvents();
+ },
+ /**
+ * Change the key for an existing item in the collection. If the old key
+ * does not exist this is a no-op.
+ * @param {Object} oldKey The old key
+ * @param {Object} newKey The new key
+ */
+ updateKey: function(oldKey, newKey) {
+ var me = this,
+ map = me.map,
+ index = me.indexOfKey(oldKey),
+ // Important: Take reference to indexMap AFTER indexOf call which may rebuild it.
+ indexMap = me.indexMap,
+ item;
+ if (index > -1) {
+ item = map[oldKey];
+ delete map[oldKey];
+ delete indexMap[oldKey];
+ map[newKey] = item;
+ indexMap[newKey] = index;
+ me.keys[index] = newKey;
+ // indexGeneration will be in sync since we called indexOfKey
+ // And we kept it all in sync, so now generation changes we keep the indexGeneration
+ // matched
+ me.indexGeneration = ++me.generation;
+ }
+ },
+ /**
+ * Adds all elements of an Array or an Object to the collection.
+ * @param {Object/Array} objs An Object containing properties which will be added
+ * to the collection, or an Array of values, each of which are added to the collection.
+ * Functions references will be added to the collection if `{@link #allowFunctions}`
+ * has been set to `true`.
+ * @since 1.1.0
+ */
+ addAll: function(objs) {
+ var me = this,
+ key;
+ if (arguments.length > 1 || Ext.isArray(objs)) {
+ me.insert(me.length, arguments.length > 1 ? arguments : objs);
+ } else {
+ for (key in objs) {
+ if (objs.hasOwnProperty(key)) {
+ if (me.allowFunctions || typeof objs[key] !== 'function') {
+ me.add(key, objs[key]);
+ }
+ }
+ }
+ }
+ },
+ /**
+ * Executes the specified function once for every item in the collection.
+ * The function should return a boolean value.
+ * Returning false from the function will stop the iteration.
+ *
+ * @param {Function} fn The function to execute for each item.
+ * @param {Mixed} fn.item The collection item.
+ * @param {Number} fn.index The index of item.
+ * @param {Number} fn.len Total length of collection.
+ * @param {Object} scope (optional) The scope (this
reference)
+ * in which the function is executed. Defaults to the current item in the iteration.
+ *
+ * @since 1.1.0
+ */
+ each: function(fn, scope) {
+ var items = Ext.Array.push([], this.items),
+ // each safe for removal
+ i = 0,
+ len = items.length,
+ item;
+ for (; i < len; i++) {
+ item = items[i];
+ if (fn.call(scope || item, item, i, len) === false) {
+ break;
+ }
+ }
+ },
+ /**
+ * Executes the specified function once for every key in the collection, passing each
+ * key, and its associated item as the first two parameters.
+ * @param {Function} fn The function to execute for each item.
+ * @param {String} fn.key The key of collection item.
+ * @param {Mixed} fn.item The collection item.
+ * @param {Number} fn.index The index of item.
+ * @param {Number} fn.len Total length of collection.
+ * @param {Object} scope (optional) The scope (this
reference) in which the
+ * function is executed. Defaults to the browser window.
+ *
+ * @since 1.1.0
+ */
+ eachKey: function(fn, scope) {
+ var keys = this.keys,
+ items = this.items,
+ i = 0,
+ len = keys.length;
+ for (; i < len; i++) {
+ fn.call(scope || window, keys[i], items[i], i, len);
+ }
+ },
+ /**
+ * Returns the first item in the collection which elicits a true return value from the
+ * passed selection function.
+ * @param {Function} fn The selection function to execute for each item.
+ * @param {Mixed} fn.item The collection item.
+ * @param {String} fn.key The key of collection item.
+ * @param {Object} scope (optional) The scope (this
reference) in which the
+ * function is executed. Defaults to the browser window.
+ * @return {Object} The first item in the collection which returned true from the selection
+ * function, or null if none was found.
+ */
+ findBy: function(fn, scope) {
+ var keys = this.keys,
+ items = this.items,
+ i = 0,
+ len = items.length;
+ for (; i < len; i++) {
+ if (fn.call(scope || window, items[i], keys[i])) {
+ return items[i];
+ }
+ }
+ return null;
+ },
+ /**
+ * Returns the first item in the collection which elicits a true return value from the
+ * passed selection function.
+ * @deprecated 4.0 Use {@link #findBy} instead.
+ * @since 1.1.0
+ */
+ find: function() {
+ if (Ext.isDefined(Ext.global.console)) {
+ Ext.global.console.warn('Ext.util.MixedCollection: find has been deprecated. ' + 'Use findBy instead.');
+ }
+ return this.findBy.apply(this, arguments);
+ },
+ /**
+ * Inserts an item at the specified index in the collection. Fires the {@link #event-add}
+ * event when complete.
+ * @param {Number} index The index to insert the item at.
+ * @param {String/Object/String[]/Object[]} key The key to associate with the new item,
+ * or the item itself. May also be an array of either to insert multiple items at once.
+ * @param {Object/Object[]} obj (optional) If the second parameter was a key, the new item.
+ * May also be an array to insert multiple items at once.
+ * @return {Object} The item inserted or an array of items inserted.
+ * @since 1.1.0
+ */
+ insert: function(index, key, obj) {
+ var out;
+ if (Ext.isIterable(key)) {
+ out = this.doInsert(index, key, obj);
+ } else {
+ if (arguments.length > 2) {
+ out = this.doInsert(index, [
+ key
+ ], [
+ obj
+ ]);
+ } else {
+ out = this.doInsert(index, [
+ key
+ ]);
+ }
+ out = out[0];
+ }
+ return out;
+ },
+ // Private multi insert implementation.
+ doInsert: function(index, keys, objects) {
+ var me = this,
+ itemKey, removeIndex, i,
+ len = keys.length,
+ deDupedLen = len,
+ fireAdd = me.hasListeners.add,
+ syncIndices,
+ newKeys = {},
+ passedDuplicates, oldKeys, oldObjects;
+ // External key(s) passed. We cannot reliably find an object's index using the
+ // key extraction fn. Set a flag for use by contains, indexOf and remove
+ if (objects != null) {
+ me.useLinearSearch = true;
+ } else // No external keys: calculate keys array if not passed
+ {
+ objects = keys;
+ keys = new Array(len);
+ for (i = 0; i < len; i++) {
+ keys[i] = this.getKey(objects[i]);
+ }
+ }
+ // First, remove duplicates of the keys. If a removal point is less than insertion index,
+ // decr insertion index.
+ me.suspendEvents();
+ for (i = 0; i < len; i++) {
+ itemKey = keys[i];
+ // Must use indexOf - map might be out of sync
+ removeIndex = me.indexOfKey(itemKey);
+ if (removeIndex !== -1) {
+ if (removeIndex < index) {
+ index--;
+ }
+ me.removeAt(removeIndex);
+ }
+ if (itemKey != null) {
+ // If a previous new item used this key, we will have to rebuild the input arrays
+ // from the newKeys map.
+ if (newKeys[itemKey] != null) {
+ passedDuplicates = true;
+ deDupedLen--;
+ }
+ newKeys[itemKey] = i;
+ }
+ }
+ me.resumeEvents();
+ // Duplicate keys were detected - rebuild the objects and keys arrays from the last values
+ // associated with each unique key
+ if (passedDuplicates) {
+ oldKeys = keys;
+ oldObjects = objects;
+ keys = new Array(deDupedLen);
+ objects = new Array(deDupedLen);
+ i = 0;
+ // Loop through unique key hash, properties of which point to last encountered index
+ // for that key. Rebuild deduped objects and keys arrays.
+ for (itemKey in newKeys) {
+ keys[i] = oldKeys[newKeys[itemKey]];
+ objects[i] = oldObjects[newKeys[itemKey]];
+ i++;
+ }
+ len = deDupedLen;
+ }
+ // If we are appending and the indices are in sync, its cheap to kep them that way
+ syncIndices = index === me.length && me.indexGeneration === me.generation;
+ // Insert the new items and new keys in at the insertion point
+ Ext.Array.insert(me.items, index, objects);
+ Ext.Array.insert(me.keys, index, keys);
+ me.length += len;
+ me.generation++;
+ if (syncIndices) {
+ me.indexGeneration = me.generation;
+ }
+ for (i = 0; i < len; i++ , index++) {
+ itemKey = keys[i];
+ if (itemKey != null) {
+ me.map[itemKey] = objects[i];
+ // If the index is still in sync, keep it that way
+ if (syncIndices) {
+ me.indexMap[itemKey] = index;
+ }
+ }
+ if (fireAdd) {
+ me.fireEvent('add', index, objects[i], itemKey);
+ }
+ }
+ return objects;
+ },
+ /**
+ * Remove an item from the collection.
+ * @param {Object} o The item to remove.
+ * @return {Object} The item removed or false if no item was removed.
+ * @since 1.1.0
+ */
+ remove: function(o) {
+ var me = this,
+ removeKey, index;
+ // If
+ // We have not been forced into using linear lookup by a usage of the 2 arg form of add
+ // and
+ // The key extraction function yields a key
+ // Then use indexOfKey. This will use the indexMap - rebuilding it if necessary.
+ if (!me.useLinearSearch && (removeKey = me.getKey(o))) {
+ index = me.indexOfKey(removeKey);
+ } else // Otherwise we have to do it the slow way with a linear search.
+ {
+ index = Ext.Array.indexOf(me.items, o);
+ }
+ return (index === -1) ? false : me.removeAt(index);
+ },
+ /**
+ * Remove all items in the collection. Can also be used
+ * to remove only the items in the passed array.
+ * @param {Array} [items] An array of items to be removed.
+ * @return {Ext.util.MixedCollection} this object
+ */
+ removeAll: function(items) {
+ var me = this,
+ i;
+ if (items || me.hasListeners.remove) {
+ // Only perform expensive item-by-item removal if there's a listener or specific items
+ if (items) {
+ for (i = items.length - 1; i >= 0; --i) {
+ me.remove(items[i]);
+ }
+ } else {
+ while (me.length) {
+ me.removeAt(0);
+ }
+ }
+ } else {
+ me.length = me.items.length = me.keys.length = 0;
+ me.map = {};
+ me.indexMap = {};
+ me.generation++;
+ me.indexGeneration = me.generation;
+ }
+ },
+ /**
+ * Remove an item from a specified index in the collection. Fires the {@link #event-remove}
+ * event when complete.
+ * @param {Number} index The index within the collection of the item to remove.
+ * @return {Object} The item removed or false if no item was removed.
+ * @since 1.1.0
+ */
+ removeAt: function(index) {
+ var me = this,
+ o, key;
+ if (index < me.length && index >= 0) {
+ me.length--;
+ o = me.items[index];
+ Ext.Array.erase(me.items, index, 1);
+ key = me.keys[index];
+ if (typeof key !== 'undefined') {
+ delete me.map[key];
+ }
+ Ext.Array.erase(me.keys, index, 1);
+ if (me.hasListeners.remove) {
+ me.fireEvent('remove', o, key);
+ }
+ me.generation++;
+ return o;
+ }
+ return false;
+ },
+ /**
+ * Remove a range of items starting at a specified index in the collection.
+ * Does not fire the remove event.
+ * @param {Number} index The index within the collection of the item to remove.
+ * @param {Number} [removeCount=1] The nuber of items to remove beginning at the
+ * specified index.
+ * @return {Object} The last item removed or false if no item was removed.
+ */
+ removeRange: function(index, removeCount) {
+ var me = this,
+ o, key, i, limit, syncIndices, trimming;
+ if (index < me.length && index >= 0) {
+ if (!removeCount) {
+ removeCount = 1;
+ }
+ limit = Math.min(index + removeCount, me.length);
+ removeCount = limit - index;
+ // If we are removing from end and the indices are in sync, its cheap to keep
+ // them that way
+ trimming = limit === me.length;
+ syncIndices = trimming && me.indexGeneration === me.generation;
+ // Loop through the to remove indices deleting from the key hashes
+ for (i = index; i < limit; i++) {
+ key = me.keys[i];
+ if (key != null) {
+ delete me.map[key];
+ if (syncIndices) {
+ delete me.indexMap[key];
+ }
+ }
+ }
+ // Last item encountered
+ o = me.items[i - 1];
+ me.length -= removeCount;
+ me.generation++;
+ if (syncIndices) {
+ me.indexGeneration = me.generation;
+ }
+ // Chop items and keys arrays.
+ // If trimming the trailing end, we can just truncate the array.
+ // We can use splice directly. The IE8 bug which Ext.Array works around only affects
+ // *insertion*
+ // http://social.msdn.microsoft.com/Forums/en-US/iewebdevelopment/thread/6e946d03-e09f-4b22-a4dd-cd5e276bf05a/
+ if (trimming) {
+ me.items.length = me.keys.length = me.length;
+ } else {
+ me.items.splice(index, removeCount);
+ me.keys.splice(index, removeCount);
+ }
+ // Return last object removed
+ return o;
+ }
+ return false;
+ },
+ /**
+ * Removes an item associated with the passed key fom the collection.
+ * @param {String} key The key of the item to remove. If `null` is passed,
+ * all objects which yielded no key from the configured {@link #getKey} function are removed.
+ * @return {Object} Only returned if removing at a specified key. The item removed or false
+ * if no item was removed.
+ */
+ removeAtKey: function(key) {
+ var me = this,
+ keys = me.keys,
+ i;
+ // Remove objects which yielded no key from our configured getKey function
+ if (key == null) {
+ for (i = keys.length - 1; i >= 0; i--) {
+ if (keys[i] == null) {
+ me.removeAt(i);
+ }
+ }
+ } else // Remove object at the passed key
+ {
+ return me.removeAt(me.indexOfKey(key));
+ }
+ },
+ /**
+ * Returns the number of items in the collection.
+ * @return {Number} the number of items in the collection.
+ * @since 1.1.0
+ */
+ getCount: function() {
+ return this.length;
+ },
+ /**
+ * Returns index within the collection of the passed Object.
+ * @param {Object} o The item to find the index of.
+ * @return {Number} index of the item. Returns -1 if not found.
+ * @since 1.1.0
+ */
+ indexOf: function(o) {
+ var me = this,
+ key;
+ if (o != null) {
+ // If
+ // We have not been forced into using linear lookup by a usage of the 2 arg form
+ // of add
+ // and
+ // The key extraction function yields a key
+ // Then use indexOfKey. This will use the indexMap - rebuilding it if necessary.
+ if (!me.useLinearSearch && (key = me.getKey(o))) {
+ return this.indexOfKey(key);
+ }
+ // Fallback: Use linear search
+ return Ext.Array.indexOf(me.items, o);
+ }
+ // No object passed
+ return -1;
+ },
+ /**
+ * Returns index within the collection of the passed key.
+ * @param {String} key The key to find the index of.
+ * @return {Number} index of the key.
+ * @since 1.1.0
+ */
+ indexOfKey: function(key) {
+ if (!this.map.hasOwnProperty(key)) {
+ return -1;
+ }
+ if (this.indexGeneration !== this.generation) {
+ this.rebuildIndexMap();
+ }
+ return this.indexMap[key];
+ },
+ rebuildIndexMap: function() {
+ var me = this,
+ indexMap = me.indexMap = {},
+ keys = me.keys,
+ len = keys.length,
+ i;
+ for (i = 0; i < len; i++) {
+ indexMap[keys[i]] = i;
+ }
+ me.indexGeneration = me.generation;
+ },
+ /**
+ * Returns the item associated with the passed key OR index.
+ * Key has priority over index. This is the equivalent
+ * of calling {@link #getByKey} first, then if nothing matched calling {@link #getAt}.
+ * @param {String/Number} key The key or index of the item.
+ * @return {Object} If the item is found, returns the item. If the item was not found,
+ * returns `undefined`. If an item was found, but is a Class, returns `null`.
+ * @since 1.1.0
+ */
+ get: function(key) {
+ var me = this,
+ mk = me.map[key],
+ item = mk !== undefined ? mk : (typeof key === 'number') ? me.items[key] : undefined;
+ return typeof item !== 'function' || me.allowFunctions ? item : null;
+ },
+ // for prototype!
+ /**
+ * Returns the item at the specified index.
+ * @param {Number} index The index of the item.
+ * @return {Object} The item at the specified index.
+ */
+ getAt: function(index) {
+ return this.items[index];
+ },
+ /**
+ * Returns the item associated with the passed key.
+ * @param {String/Number} key The key of the item.
+ * @return {Object} The item associated with the passed key.
+ */
+ getByKey: function(key) {
+ return this.map[key];
+ },
+ /**
+ * Returns true if the collection contains the passed Object as an item.
+ * @param {Object} o The Object to look for in the collection.
+ * @return {Boolean} True if the collection contains the Object as an item.
+ * @since 1.1.0
+ */
+ contains: function(o) {
+ var me = this,
+ key;
+ if (o != null) {
+ // If
+ // We have not been forced into using linear lookup by a usage of the 2 arg form
+ // of add
+ // and
+ // The key extraction function yields a key
+ // Then use the map to determine object presence.
+ if (!me.useLinearSearch && (key = me.getKey(o))) {
+ return this.map[key] != null;
+ }
+ // Fallback: Use linear search
+ return Ext.Array.indexOf(this.items, o) !== -1;
+ }
+ return false;
+ },
+ /**
+ * Returns true if the collection contains the passed Object as a key.
+ * @param {String} key The key to look for in the collection.
+ * @return {Boolean} True if the collection contains the Object as a key.
+ * @since 1.1.0
+ */
+ containsKey: function(key) {
+ return this.map.hasOwnProperty(key);
+ },
+ /**
+ * Removes all items from the collection. Fires the {@link #event-clear} event when complete.
+ * @since 1.1.0
+ */
+ clear: function() {
+ var me = this;
+ // Only clear if it has ever had any content
+ if (me.generation) {
+ me.length = 0;
+ me.items = [];
+ me.keys = [];
+ me.map = {};
+ me.indexMap = {};
+ me.generation++;
+ me.indexGeneration = me.generation;
+ }
+ if (me.hasListeners.clear) {
+ me.fireEvent('clear');
+ }
+ },
+ /**
+ * Returns the first item in the collection.
+ * @return {Object} the first item in the collection..
+ * @since 1.1.0
+ */
+ first: function() {
+ return this.items[0];
+ },
+ /**
+ * Returns the last item in the collection.
+ * @return {Object} the last item in the collection..
+ * @since 1.1.0
+ */
+ last: function() {
+ return this.items[this.length - 1];
+ },
+ /**
+ * Collects all of the values of the given property and returns their sum
+ * @param {String} property The property to sum by
+ * @param {String} [root] 'root' property to extract the first argument from. This is used
+ * mainly when summing fields in records, where the fields are all stored inside the 'data'
+ * object
+ * @param {Number} [start=0] The record index to start at
+ * @param {Number} [end=-1] The record index to end at
+ * @return {Number} The total
+ */
+ sum: function(property, root, start, end) {
+ var values = this.extractValues(property, root),
+ length = values.length,
+ sum = 0,
+ i;
+ start = start || 0;
+ end = (end || end === 0) ? end : length - 1;
+ for (i = start; i <= end; i++) {
+ sum += values[i];
+ }
+ return sum;
+ },
+ /**
+ * Collects unique values of a particular property in this MixedCollection
+ * @param {String} property The property to collect on
+ * @param {String} root (optional) 'root' property to extract the first argument from. This is
+ * used mainly when summing fields in records, where the fields are all stored inside the 'data'
+ * object
+ * @param {Boolean} allowNull (optional) Pass true to allow null, undefined or empty string
+ * values
+ * @return {Array} The unique values
+ */
+ collect: function(property, root, allowNull) {
+ var values = this.extractValues(property, root),
+ length = values.length,
+ hits = {},
+ unique = [],
+ value, strValue, i;
+ for (i = 0; i < length; i++) {
+ value = values[i];
+ strValue = String(value);
+ if ((allowNull || !Ext.isEmpty(value)) && !hits[strValue]) {
+ hits[strValue] = true;
+ unique.push(value);
+ }
+ }
+ return unique;
+ },
+ /**
+ * @private
+ * Extracts all of the given property values from the items in the MC. Mainly used as a
+ * supporting method for functions like sum and collect.
+ * @param {String} property The property to extract
+ * @param {String} root (optional) 'root' property to extract the first argument from.
+ * This is used mainly when extracting field data from Model instances, where the fields
+ * are stored inside the 'data' object
+ * @return {Array} The extracted values
+ */
+ extractValues: function(property, root) {
+ var values = this.items;
+ if (root) {
+ values = Ext.Array.pluck(values, root);
+ }
+ return Ext.Array.pluck(values, property);
+ },
+ /**
+ * @private
+ * For API parity with Store's PageMap class. Buffered rendering checks if the Store has
+ * the range required to render. The Store delegates this question to its backing data object
+ * which may be an instance of its private PageMap class, or a MixedCollection.
+ */
+ hasRange: function(start, end) {
+ return (end < this.length);
+ },
+ /**
+ * Returns a range of items in this collection
+ * @param {Number} start (optional) The starting index. Defaults to 0.
+ * @param {Number} end (optional) The ending index. Defaults to the last item.
+ * @return {Array} An array of items
+ * @since 1.1.0
+ */
+ getRange: function(start, end) {
+ var me = this,
+ items = me.items,
+ range = [],
+ len = items.length,
+ tmp, reverse;
+ if (len < 1) {
+ return range;
+ }
+ if (start > end) {
+ reverse = true;
+ tmp = start;
+ start = end;
+ end = tmp;
+ }
+ if (start < 0) {
+ start = 0;
+ }
+ if (end == null || end >= len) {
+ end = len - 1;
+ }
+ range = items.slice(start, end + 1);
+ if (reverse && range.length) {
+ range.reverse();
+ }
+ return range;
+ },
+ /**
+ * Filters the objects in this collection by a set of {@link Ext.util.Filter Filter}s, or by
+ * a single property/value pair with optional parameters for substring matching and
+ * case sensitivity. See {@link Ext.util.Filter Filter} for an example of using Filter objects
+ * (preferred). Alternatively, MixedCollection can be easily filtered by property like this:
+ *
+ * // create a simple store with a few people defined
+ * var people = new Ext.util.MixedCollection();
+ * people.addAll([
+ * {id: 1, age: 25, name: 'Ed'},
+ * {id: 2, age: 24, name: 'Tommy'},
+ * {id: 3, age: 24, name: 'Arne'},
+ * {id: 4, age: 26, name: 'Aaron'}
+ * ]);
+ *
+ * // a new MixedCollection containing only the items where age == 24
+ * var middleAged = people.filter('age', 24);
+ *
+ * @param {Ext.util.Filter[]/String} property A property on your objects, or an array
+ * of {@link Ext.util.Filter Filter} objects
+ * @param {String/RegExp} value Either string that the property values
+ * should start with or a RegExp to test against the property
+ * @param {Boolean} [anyMatch=false] True to match any part of the string,
+ * not just the beginning
+ * @param {Boolean} [caseSensitive=false] True for case sensitive comparison.
+ * @return {Ext.util.MixedCollection} The new filtered collection
+ * @since 1.1.0
+ */
+ filter: function(property, value, anyMatch, caseSensitive) {
+ var filters = [];
+ // support for the simple case of filtering by property/value
+ if (Ext.isString(property)) {
+ filters.push(new Ext.util.Filter({
+ property: property,
+ value: value,
+ anyMatch: anyMatch,
+ caseSensitive: caseSensitive
+ }));
+ } else if (Ext.isArray(property) || property instanceof Ext.util.Filter) {
+ filters = filters.concat(property);
+ }
+ // At this point we have an array of zero or more Ext.util.Filter objects to filter with,
+ // so here we construct a function that combines these filters by ANDing them together
+ // and filter by that.
+ return this.filterBy(Ext.util.Filter.createFilterFn(filters));
+ },
+ /**
+ * Filter by a function. Returns a new collection that has been filtered.
+ * The passed function will be called with each object in the collection.
+ * If the function returns true, the value is included otherwise it is filtered.
+ * @param {Function} fn The function to be called.
+ * @param {Mixed} fn.item The collection item.
+ * @param {String} fn.key The key of collection item.
+ * @param {Object} scope (optional) The scope (this
reference) in
+ * which the function is executed. Defaults to this MixedCollection.
+ * @return {Ext.util.MixedCollection} The new filtered collection
+ * @since 1.1.0
+ */
+ filterBy: function(fn, scope) {
+ var me = this,
+ newMC = new me.self(me.initialConfig),
+ keys = me.keys,
+ items = me.items,
+ length = items.length,
+ i;
+ newMC.getKey = me.getKey;
+ for (i = 0; i < length; i++) {
+ if (fn.call(scope || me, items[i], keys[i])) {
+ newMC.add(keys[i], items[i]);
+ }
+ }
+ // The add using an external key will make the newMC think that keys cannot be reliably
+ // extracted from objects, so that an indexOf call will always have to do a linear search.
+ // If the flag is not set in this object, we know that the clone will not need it either.
+ newMC.useLinearSearch = me.useLinearSearch;
+ return newMC;
+ },
+ /**
+ * Finds the index of the first matching object in this collection by a specific property/value.
+ * @param {String} property The name of a property on your objects.
+ * @param {String/RegExp} value A string that the property values
+ * should start with or a RegExp to test against the property.
+ * @param {Number} [start=0] The index to start searching at.
+ * @param {Boolean} [anyMatch=false] True to match any part of the string,
+ * not just the beginning.
+ * @param {Boolean} [caseSensitive=false] True for case sensitive comparison.
+ * @return {Number} The matched index or -1
+ * @since 2.3.0
+ */
+ findIndex: function(property, value, start, anyMatch, caseSensitive) {
+ if (Ext.isEmpty(value, false)) {
+ return -1;
+ }
+ value = this.createValueMatcher(value, anyMatch, caseSensitive);
+ return this.findIndexBy(function(o) {
+ return o && value.test(o[property]);
+ }, null, start);
+ },
+ /**
+ * Find the index of the first matching object in this collection by a function.
+ * If the function returns true it is considered a match.
+ * @param {Function} fn The function to be called.
+ * @param {Mixed} fn.item The collection item.
+ * @param {String} fn.key The key of collection item.
+ * @param {Object} [scope] The scope (this
reference) in which the function
+ * is executed. Defaults to this MixedCollection.
+ * @param {Number} [start=0] The index to start searching at.
+ * @return {Number} The matched index or -1
+ * @since 2.3.0
+ */
+ findIndexBy: function(fn, scope, start) {
+ var me = this,
+ keys = me.keys,
+ items = me.items,
+ i = start || 0,
+ len = items.length;
+ for (; i < len; i++) {
+ if (fn.call(scope || me, items[i], keys[i])) {
+ return i;
+ }
+ }
+ return -1;
+ },
+ /**
+ * Returns a regular expression based on the given value and matching options. This is used
+ * internally for finding and filtering, and by Ext.data.Store#filter
+ * @private
+ * @param {String} value The value to create the regex for. This is escaped using Ext.escapeRe
+ * @param {Boolean} anyMatch True to allow any match - no regex start/end line anchors
+ * will be added. Defaults to false
+ * @param {Boolean} caseSensitive True to make the regex case sensitive
+ * (adds 'i' switch to regex). Defaults to false.
+ * @param {Boolean} exactMatch True to force exact match (^ and $ characters added
+ * to the regex). Defaults to false. Ignored if anyMatch is true.
+ * @since 3.4.0
+ */
+ createValueMatcher: function(value, anyMatch, caseSensitive, exactMatch) {
+ var er;
+ if (!value.exec) {
+ // not a regex
+ er = Ext.String.escapeRegex;
+ value = String(value);
+ if (anyMatch === true) {
+ value = er(value);
+ } else {
+ value = '^' + er(value);
+ if (exactMatch === true) {
+ value += '$';
+ }
+ }
+ value = new RegExp(value, caseSensitive ? '' : 'i');
+ }
+ return value;
+ },
+ /**
+ * Creates a shallow copy of this collection
+ * @return {Ext.util.MixedCollection}
+ * @since 1.1.0
+ */
+ clone: function() {
+ var me = this,
+ copy = new me.self(me.initialConfig);
+ copy.add(me.keys, me.items);
+ // The add using external keys will make the clone think that keys cannot be reliably
+ // extracted from objects, so that an indexOf call will always have to do a linear search.
+ // If the flag is not set in this object, we know that the clone will not need it either.
+ copy.useLinearSearch = me.useLinearSearch;
+ return copy;
+ }
+});
+
+/**
+ * Represents a single sorter that can be used as part of the sorters configuration in
+ * Ext.mixin.Sortable.
+ *
+ * A common place for Sorters to be used are {@link Ext.data.Store Stores}. For example:
+ *
+ * @example
+ * var store = Ext.create('Ext.data.Store', {
+ * fields: ['firstName', 'level'],
+ * sorters: 'level',
+ *
+ * data: [
+ * { firstName: 'Mitch', level: 9000},
+ * { firstName: 'Seth', level: 42},
+ * { firstName: 'Fred', level: 510},
+ * { firstName: 'Israel', level: 690},
+ * { firstName: 'Greg', level: 101},
+ * { firstName: 'Pat', level: 0},
+ * { firstName: 'Kevin', level: 17},
+ * { firstName: 'Brandon',level: 690},
+ * { firstName: 'Gary', level: 409},
+ * { firstName: 'Scott', level: 789}
+ * ]
+ * });
+ *
+ * Ext.create('Ext.grid.Panel', {
+ * title: 'Support',
+ * store: store,
+ * columns: [
+ * { text: 'Name', dataIndex: 'firstName' },
+ * { text: 'Level', dataIndex: 'level' }
+ * ],
+ * height: 300,
+ * width: 200,
+ * renderTo: Ext.getBody()
+ * });
+ *
+ * In the next example, we specify a custom sorter function:
+ *
+ * @example
+ * var store = Ext.create('Ext.data.Store', {
+ * fields: ['firstName', 'spiritAnimal'],
+ * sorters: [
+ * {
+ * // Sort by first letter of second word of spirit animal, in
+ * // descending order
+ * sorterFn: function(record1, record2) {
+ * var name1 = record1.data.spiritAnimal.split(' ')[1].substr(0,1),
+ * name2 = record2.data.spiritAnimal.split(' ')[1].substr(0,1);
+ *
+ * return name1 > name2 ? 1 : (name1 === name2) ? 0 : -1;
+ * },
+ * direction: 'DESC'
+ * }
+ * ],
+ *
+ * data: [
+ * { firstName: 'Mitch', spiritAnimal: "Panda Bear"},
+ * { firstName: 'Seth', spiritAnimal: "Rascally Rabbit"},
+ * { firstName: 'Fred', spiritAnimal: "Honey Badger"},
+ * { firstName: 'Israel', spiritAnimal: "Mysterious Capybara"},
+ * { firstName: 'Greg', spiritAnimal: "Majestic Platypus"},
+ * { firstName: 'Kevin', spiritAnimal: "Sparkling Unicorn"},
+ * { firstName: 'Brandon',spiritAnimal: "Pygmy Goat"},
+ * { firstName: 'Gary', spiritAnimal: "Suri Alpaca"},
+ * { firstName: 'Scott', spiritAnimal: "Ripe Armadillo"},
+ * { firstName: 'Pat', spiritAnimal: "Wiley Coyote"}
+ * ]
+ * });
+ *
+ * Ext.create('Ext.grid.Panel', {
+ * title: 'Support',
+ * store: store,
+ * columns: [
+ * { text: 'Name', dataIndex: 'firstName' },
+ * { text: 'Spirit Animal', dataIndex: 'spiritAnimal', flex: 1 }
+ * ],
+ * height: 310,
+ * renderTo: Ext.getBody()
+ * });
+ */
+Ext.define('Ext.util.Sorter', {
+ isSorter: true,
+ config: {
+ /**
+ * @cfg {String} property
+ * The property to sort by. Required unless `sorterFn` is provided
+ */
+ property: null,
+ /**
+ * @cfg {Function} sorterFn
+ * A specific sorter function to execute. Can be passed instead of {@link #property}.
+ * This function should compare the two passed arguments, returning -1, 0 or 1
+ * depending on if item 1 should be sorted before, at the same level, or after
+ * item 2.
+ *
+ * sorterFn: function(person1, person2) {
+ * return (person1.age > person2.age) ? 1 : (person1.age === person2.age ? 0 : -1);
+ * }
+ */
+ sorterFn: null,
+ /**
+ * @cfg {String} root Optional root property. This is mostly useful when sorting a Store,
+ * in which case we set the root to 'data' to make the filter pull the {@link #property}
+ * out of the data object of each item
+ */
+ root: null,
+ /**
+ * @cfg {Function} transform A function that will be run on each value before
+ * it is compared in the sorter. The function will receive a single argument,
+ * the value.
+ */
+ transform: null,
+ /**
+ * @cfg {String} direction The direction to sort by. Valid values are "ASC", and "DESC".
+ */
+ direction: "ASC",
+ /**
+ * @cfg {Mixed} id An optional id this sorter can be keyed by in Collections. If
+ * no id is specified it will use the property name used in this Sorter. If no
+ * property is specified, e.g. when adding a custom sorter function we will generate
+ * a random id.
+ */
+ id: undefined
+ },
+ statics: {
+ /**
+ * Creates a comparator function (a function that can be passed to `Array.sort`)
+ * given one or more `Sorter` instances.
+ *
+ * The returned function retains a reference to the collection or array of sorters
+ * passed. This means the function will produce a comparison based on the current
+ * content of the collection or array, and not based on the content at the time of
+ * this call.
+ *
+ * @param {Ext.util.Sorter[]/Ext.util.Collection} sorters The `Sorter` instances.
+ * @param [nextFn] The next comparator function to call if all the `sorters` end
+ * with equality.
+ * @return {Function} The comparator function.
+ */
+ createComparator: function(sorters, nextFn) {
+ nextFn = nextFn || 0;
+ return function(lhs, rhs) {
+ var items = sorters.isCollection ? sorters.items : sorters,
+ n = items.length,
+ comp, i;
+ for (i = 0; i < n; ++i) {
+ comp = items[i].sort(lhs, rhs);
+ if (comp) {
+ return comp;
+ }
+ }
+ return nextFn && nextFn(lhs, rhs);
+ };
+ }
+ },
+ /**
+ * This value is set based on the `direction` config to be either 1 or -1. This is used
+ * as a multiplier for the raw comparison value to factor in the direction.
+ * @private
+ * @readonly
+ */
+ multiplier: 1,
+ constructor: function(config) {
+ if (config && !this.isGrouper) {
+ if (!config.property === !config.sorterFn) {
+ // the above is a "not XOR" - both true or both false
+ Ext.raise("A Sorter requires either a property or a sorterFn.");
+ }
+ }
+ this.initConfig(config);
+ },
+ getId: function() {
+ var id = this._id;
+ if (!id) {
+ id = this.getProperty();
+ if (!id) {
+ id = Ext.id(null, 'ext-sorter-');
+ }
+ this._id = id;
+ }
+ return id;
+ },
+ sort: function(lhs, rhs) {
+ return this.multiplier * this.sortFn(lhs, rhs);
+ },
+ /**
+ * @private
+ * Basic default sorter function that just compares the defined property of each object.
+ * This is hidden by the `sorterFn` provided by the user.
+ */
+ sortFn: function(item1, item2) {
+ var me = this,
+ transform = me._transform,
+ root = me._root,
+ property = me._property,
+ lhs, rhs;
+ if (root) {
+ item1 = item1[root];
+ item2 = item2[root];
+ }
+ lhs = item1[property];
+ rhs = item2[property];
+ if (transform) {
+ lhs = transform(lhs);
+ rhs = transform(rhs);
+ }
+ return (lhs > rhs) ? 1 : (lhs < rhs ? -1 : 0);
+ },
+ applyDirection: function(direction) {
+ return direction ? direction : 'ASC';
+ },
+ updateDirection: function(direction) {
+ this.multiplier = (direction.toUpperCase() === "DESC") ? -1 : 1;
+ },
+ updateProperty: function(property) {
+ if (property) {
+ // Unhide the default sortFn on our prototype
+ delete this.sortFn;
+ }
+ },
+ updateSorterFn: function(sorterFn) {
+ // Hide the default sortFn on our prototype
+ this.sortFn = sorterFn;
+ },
+ /**
+ * Toggles the direction of this Sorter. Note that when you call this function,
+ * the Collection this Sorter is part of does not get refreshed automatically.
+ */
+ toggle: function() {
+ this.setDirection(Ext.String.toggle(this.getDirection(), "ASC", "DESC"));
+ },
+ /**
+ * Returns this sorter's state.
+ * @return {Object}
+ */
+ getState: function() {
+ var me = this,
+ result = {
+ root: me.getRoot(),
+ property: me.getProperty(),
+ direction: me.getDirection()
+ };
+ // Do not use getId() which will create an identifier if we have none.
+ // We need to know if we really are identifiable.
+ if (me._id) {
+ result.id = me._id;
+ }
+ return result;
+ },
+ /**
+ * Returns this sorter's serialized state. This is used when transmitting this sorter
+ * to a server.
+ * @return {Object}
+ */
+ serialize: function() {
+ return {
+ property: this.getProperty(),
+ direction: this.getDirection()
+ };
+ }
+});
+
+/**
+ * A mixin which allows a data component to be sorted. This is used by e.g. {@link Ext.data.Store}
+ * and {@link Ext.data.TreeStore}.
+ *
+ * **NOTE**: This mixin is mainly for internal use and most users should not need to use it
+ * directly. It is more likely you will want to use one of the component classes that import
+ * this mixin, such as {@link Ext.data.Store} or {@link Ext.data.TreeStore}.
+ */
+Ext.define("Ext.util.Sortable", {
+ /**
+ * @property {Boolean} isSortable
+ * `true` in this class to identify an object as an instantiated Sortable, or subclass thereof.
+ */
+ isSortable: true,
+ $configPrefixed: false,
+ $configStrict: false,
+ config: {
+ /**
+ * @cfg {Ext.util.Sorter[]/Object[]} sorters
+ * The initial set of {@link Ext.util.Sorter Sorters}.
+ *
+ * sorters: [{
+ * property: 'age',
+ * direction: 'DESC'
+ * }, {
+ * property: 'firstName',
+ * direction: 'ASC'
+ * }]
+ */
+ sorters: null
+ },
+ /**
+ * @cfg {String} defaultSortDirection
+ * The default sort direction to use if one is not specified.
+ */
+ defaultSortDirection: "ASC",
+ /**
+ * @event beforesort
+ * Fires before a sort occurs.
+ * @param {Ext.util.Sortable} me This object.
+ * @param {Ext.util.Sorter[]} sorters The collection of Sorters being used to generate
+ * the comparator function.
+ */
+ /**
+ * @cfg {Number} [multiSortLimit=3]
+ * The maximum number of sorters which may be applied to this Sortable when using the "multi"
+ * insertion position when adding sorters.
+ *
+ * New sorters added using the "multi" insertion position are inserted at the top of the
+ * sorters list becoming the new primary sort key.
+ *
+ * If the sorters collection has grown to longer then **`multiSortLimit`**, then it is trimmed.
+ *
+ */
+ multiSortLimit: 3,
+ statics: {
+ /**
+ * Creates a single comparator function which encapsulates the passed Sorter array.
+ * @param {Ext.util.Sorter[]} sorters The sorter set for which to create a comparator
+ * function
+ * @return {Function} a function, which when passed two comparable objects returns
+ * the result of the whole sorter comparator functions.
+ */
+ createComparator: function(sorters) {
+ return sorters && sorters.length ? function(r1, r2) {
+ var result = sorters[0].sort(r1, r2),
+ length = sorters.length,
+ i = 1;
+ // While we have not established a comparison value,
+ // loop through subsequent sorters asking for a comparison value
+ for (; !result && i < length; i++) {
+ result = sorters[i].sort.call(sorters[i], r1, r2);
+ }
+ return result;
+ } : function() {
+ return 0;
+ };
+ }
+ },
+ /**
+ * @cfg {String} sortRoot
+ * The property in each item that contains the data to sort.
+ */
+ applySorters: function(sorters) {
+ var me = this,
+ sortersCollection;
+ sortersCollection = me.getSorters() || new Ext.util.MixedCollection(false, Ext.returnId);
+ // We have been configured with a non-default value.
+ if (sorters) {
+ sortersCollection.addAll(me.decodeSorters(sorters));
+ }
+ return sortersCollection;
+ },
+ /**
+ * Updates the sorters collection and triggers sorting of this Sortable. Example usage:
+ *
+ * //sort by a single field
+ * myStore.sort('myField', 'DESC');
+ *
+ * //sorting by multiple fields
+ * myStore.sort([{
+ * property : 'age',
+ * direction: 'ASC'
+ * }, {
+ * property : 'name',
+ * direction: 'DESC'
+ * }]);
+ *
+ * Classes which use this mixin must implement a **`soSort`** method which accepts a comparator
+ * function computed from the full sorter set which performs the sort
+ * in an implementation-specific way.
+ *
+ * When passing a single string argument to sort, Store maintains a ASC/DESC toggler per field,
+ * so this code:
+ *
+ * store.sort('myField');
+ * store.sort('myField');
+ *
+ * Is equivalent to this code, because Store handles the toggling automatically:
+ *
+ * store.sort('myField', 'ASC');
+ * store.sort('myField', 'DESC');
+ *
+ * @param {String/Ext.util.Sorter[]} [sorters] Either a string name of one of the fields
+ * in this Store's configured {@link Ext.data.Model Model}, or an array of sorter
+ * configurations.
+ * @param {String} [direction="ASC"] The overall direction to sort the data by.
+ * @param {String} [insertionPosition="replace"] Where to put the new sorter in the collection
+ * of sorters. This may take the following values:
+ *
+ * * `replace`: This means that the new sorter(s) becomes the sole sorter set for this Sortable.
+ * This is the most useful call mode to programatically sort by multiple fields.
+ *
+ * * `prepend`: This means that the new sorters are inserted as the primary sorters, unchanged,
+ * and the sorter list length must be controlled by the developer.
+ *
+ * * `multi`: This is mainly useful for implementing intuitive "Sort by this" user interfaces
+ * such as the {@link Ext.grid.Panel GridPanel}'s column sorting UI. This mode is only
+ * supported when passing a property name and a direction. This means that the new sorter
+ * becomes the primary sorter. If the sorter was **already** the primary sorter, the direction
+ * of sort is toggled if no direction parameter is specified. The number of sorters maintained
+ * is limited by the {@link #multiSortLimit} configuration.
+ *
+ * * `append` : This means that the new sorter becomes the last sorter.
+ * @param {Boolean} doSort True to sort using a generated sorter function that combines all
+ * of the Sorters passed
+ * @return {Ext.util.Sorter[]} The new sorters.
+ */
+ sort: function(sorters, direction, insertionPosition, doSort) {
+ var me = this,
+ sorter, overFlow,
+ currentSorters = me.getSorters();
+ if (!currentSorters) {
+ me.setSorters(null);
+ currentSorters = me.getSorters();
+ }
+ if (Ext.isArray(sorters)) {
+ doSort = insertionPosition;
+ insertionPosition = direction;
+ } else if (Ext.isObject(sorters)) {
+ sorters = [
+ sorters
+ ];
+ doSort = insertionPosition;
+ insertionPosition = direction;
+ } else if (Ext.isString(sorters)) {
+ sorter = currentSorters.get(sorters);
+ if (!sorter) {
+ sorter = {
+ property: sorters,
+ direction: direction
+ };
+ } else if (direction == null) {
+ sorter.toggle();
+ } else {
+ sorter.setDirection(direction);
+ }
+ sorters = [
+ sorter
+ ];
+ }
+ if (sorters && sorters.length) {
+ sorters = me.decodeSorters(sorters);
+ switch (insertionPosition) {
+ // multi sorting means always inserting the specified sorters
+ // at the top.
+ // If we are asked to sort by what is already the primary sorter
+ // then toggle its direction.
+ case "multi":
+ // Insert the new sorter at the beginning.
+ currentSorters.insert(0, sorters[0]);
+ // If we now are oversize, trim our sorters collection
+ overFlow = currentSorters.getCount() - me.multiSortLimit;
+ if (overFlow > 0) {
+ currentSorters.removeRange(me.multiSortLimit, overFlow);
+ };
+ break;
+ case "prepend":
+ currentSorters.insert(0, sorters);
+ break;
+ case "append":
+ currentSorters.addAll(sorters);
+ break;
+ case undefined:
+ case null:
+ case "replace":
+ currentSorters.clear();
+ currentSorters.addAll(sorters);
+ break;
+ default:
+ Ext.raise('Sorter insertion point must be "multi", "prepend", ' + '"append" or "replace"');
+ }
+ }
+ if (doSort !== false) {
+ me.fireEvent('beforesort', me, sorters);
+ me.onBeforeSort(sorters);
+ if (me.getSorterCount()) {
+ // Sort using a generated sorter function which combines all of the Sorters passed
+ me.doSort(me.generateComparator());
+ }
+ }
+ return sorters;
+ },
+ /**
+ * @protected
+ * Returns the number of Sorters which apply to this Sortable.
+ *
+ * May be overridden in subclasses. {@link Ext.data.Store Store} in particlar overrides
+ * this because its groupers must contribute to the sorter count so that the sort method above
+ * executes doSort.
+ */
+ getSorterCount: function() {
+ return this.getSorters().items.length;
+ },
+ /**
+ * Returns a comparator function which compares two items and returns -1, 0, or 1 depending
+ * on the currently defined set of {@link #cfg-sorters}.
+ *
+ * If there are no {@link #cfg-sorters} defined, it returns a function which returns `0` meaning
+ * that no sorting will occur.
+ */
+ generateComparator: function() {
+ var sorters = this.getSorters().getRange();
+ return sorters.length ? this.createComparator(sorters) : this.emptyComparator;
+ },
+ emptyComparator: function() {
+ return 0;
+ },
+ onBeforeSort: Ext.emptyFn,
+ /**
+ * @private
+ * Normalizes an array of sorter objects, ensuring that they are all Ext.util.Sorter instances
+ * @param {Object[]} sorters The sorters array
+ * @return {Ext.util.Sorter[]} Array of Ext.util.Sorter objects
+ */
+ decodeSorters: function(sorters) {
+ if (!Ext.isArray(sorters)) {
+ if (sorters === undefined) {
+ sorters = [];
+ } else {
+ sorters = [
+ sorters
+ ];
+ }
+ }
+ // eslint-disable-next-line vars-on-top
+ var length = sorters.length,
+ Sorter = Ext.util.Sorter,
+ model = this.getModel ? this.getModel() : this.model,
+ field, config, i;
+ for (i = 0; i < length; i++) {
+ config = sorters[i];
+ if (!(config instanceof Sorter)) {
+ if (Ext.isString(config)) {
+ config = {
+ property: config
+ };
+ }
+ Ext.applyIf(config, {
+ root: this.sortRoot,
+ direction: "ASC"
+ });
+ // support for 3.x style sorters where a function can be defined as 'fn'
+ if (config.fn) {
+ config.sorterFn = config.fn;
+ }
+ // support a function to be passed as a sorter definition
+ if (typeof config === 'function') {
+ config = {
+ sorterFn: config
+ };
+ }
+ // ensure sortType gets pushed on if necessary
+ if (model && !config.transform) {
+ field = model.getField(config.property);
+ config.transform = field && field.sortType !== Ext.identityFn ? field.sortType : undefined;
+ }
+ sorters[i] = new Ext.util.Sorter(config);
+ }
+ }
+ return sorters;
+ },
+ /**
+ * Gets the first sorter from the sorters collection, excluding
+ * any groupers that may be in place
+ * @protected
+ * @return {Ext.util.Sorter} The sorter, null if none exist
+ */
+ getFirstSorter: function() {
+ var sorters = this.getSorters().items,
+ len = sorters.length,
+ i = 0,
+ sorter;
+ for (; i < len; ++i) {
+ sorter = sorters[i];
+ if (!sorter.isGrouper) {
+ return sorter;
+ }
+ }
+ return null;
+ }
+}, function() {
+ // Reference the static implementation in prototype
+ this.prototype.createComparator = this.createComparator;
+});
+
+/**
+ * Represents a collection of a set of key and value pairs. Each key in the MixedCollection
+ * must be unique, the same key cannot exist twice. This collection is ordered, items in the
+ * collection can be accessed by index or via the key. Newly added items are added to
+ * the end of the collection. This class is similar to {@link Ext.util.HashMap} however it
+ * is heavier and provides more functionality. Sample usage:
+ *
+ * var coll = new Ext.util.MixedCollection();
+ * coll.add('key1', 'val1');
+ * coll.add('key2', 'val2');
+ * coll.add('key3', 'val3');
+ *
+ * console.log(coll.get('key1')); // prints 'val1'
+ * console.log(coll.indexOfKey('key3')); // prints 2
+ *
+ * The MixedCollection also has support for sorting and filtering of the values in the collection.
+ *
+ * var coll = new Ext.util.MixedCollection();
+ * coll.add('key1', 100);
+ * coll.add('key2', -100);
+ * coll.add('key3', 17);
+ * coll.add('key4', 0);
+ * var biggerThanZero = coll.filterBy(function(value){
+ * return value > 0;
+ * });
+ * console.log(biggerThanZero.getCount()); // prints 2
+ *
+ */
+Ext.define('Ext.util.MixedCollection', {
+ extend: Ext.util.AbstractMixedCollection,
+ mixins: {
+ sortable: Ext.util.Sortable
+ },
+ /**
+ * @cfg {Boolean} allowFunctions
+ * Configure as `true` if the {@link #addAll} function should add function references
+ * to the collection.
+ */
+ /**
+ * Creates new MixedCollection.
+ * @param {Object} config A configuration object.
+ * @param {Boolean} [config.allowFunctions=false] Specify `true` if the
+ * {@link #addAll} function should add function references to the collection.
+ * @param {Function} [config.getKey] A function that can accept an item of the
+ * type(s) stored in this MixedCollection and return the key value for that item.
+ * This is used when available to look up the key on items that were passed without
+ * an explicit key parameter to a MixedCollection method. Passing this parameter is
+ * equivalent to overriding the {@link #method-getKey} method.
+ */
+ constructor: function() {
+ this.initConfig();
+ this.callParent(arguments);
+ },
+ doSort: function(sorterFn) {
+ this.sortBy(sorterFn);
+ },
+ /**
+ * @private
+ * Performs the actual sorting based on a direction and a sorting function.
+ * Internally, this creates a temporary array of all items in the MixedCollection,
+ * sorts it and then writes the sorted array data back into this.items and this.keys
+ * @param {String} property Property to sort by ('key', 'value', or 'index')
+ * @param {String} dir (optional) Direction to sort 'ASC' or 'DESC'. Defaults to
+ * 'ASC'.
+ * @param {Function} fn (optional) Comparison function that defines the sort order.
+ * Defaults to sorting by numeric value.
+ */
+ _sort: function(property, dir, fn) {
+ var me = this,
+ i, len,
+ dsc = String(dir).toUpperCase() === 'DESC' ? -1 : 1,
+ // this is a temporary array used to apply the sorting function
+ c = [],
+ keys = me.keys,
+ items = me.items,
+ o;
+ // default to a simple sorter function if one is not provided
+ fn = fn || function(a, b) {
+ return a - b;
+ };
+ // copy all the items into a temporary array, which we will sort
+ for (i = 0 , len = items.length; i < len; i++) {
+ c[c.length] = {
+ key: keys[i],
+ value: items[i],
+ index: i
+ };
+ }
+ // sort the temporary array
+ Ext.Array.sort(c, function(a, b) {
+ return fn(a[property], b[property]) * dsc || (// In case of equality, ensure stable sort by comparing collection index
+ a.index < b.index ? -1 : 1);
+ });
+ // Copy the temporary array back into the main this.items and this.keys objects
+ // Repopulate the indexMap hash if configured to do so.
+ for (i = 0 , len = c.length; i < len; i++) {
+ o = c[i];
+ items[i] = o.value;
+ keys[i] = o.key;
+ me.indexMap[o.key] = i;
+ }
+ me.generation++;
+ me.indexGeneration = me.generation;
+ me.fireEvent('sort', me);
+ },
+ /**
+ * Sorts the collection by a single sorter function
+ * @param {Function} sorterFn The function to sort by
+ */
+ sortBy: function(sorterFn) {
+ var me = this,
+ items = me.items,
+ item,
+ keys = me.keys,
+ key,
+ length = items.length,
+ i;
+ // Stamp the collection index into each item so that we can implement stable sort
+ for (i = 0; i < length; i++) {
+ items[i].$extCollectionIndex = i;
+ }
+ Ext.Array.sort(items, function(a, b) {
+ return sorterFn(a, b) || (// In case of equality, ensure stable sort by comparing collection index
+ a.$extCollectionIndex < b.$extCollectionIndex ? -1 : 1);
+ });
+ // Update the keys array, and remove the index
+ for (i = 0; i < length; i++) {
+ item = items[i];
+ key = me.getKey(item);
+ keys[i] = key;
+ me.indexMap[key] = i;
+ delete item.$extCollectionIndex;
+ }
+ me.generation++;
+ me.indexGeneration = me.generation;
+ me.fireEvent('sort', me, items, keys);
+ },
+ /**
+ * Calculates the insertion index of the new item based upon the comparison function passed,
+ * or the current sort order.
+ * @param {Object} newItem The new object to find the insertion position of.
+ * @param {Function} [sorterFn] The function to sort by. This is the same as the sorting
+ * function passed to {@link #sortBy}. It accepts 2 items from this MixedCollection,
+ * and returns -1 0, or 1 depending on the relative sort positions of the 2 compared items.
+ *
+ * If omitted, a function {@link #generateComparator generated} from the currently defined
+ * set of {@link #cfg-sorters} will be used.
+ *
+ * @return {Number} The insertion point to add the new item into this MixedCollection
+ * at using {@link #insert}
+ */
+ findInsertionIndex: function(newItem, sorterFn) {
+ var me = this,
+ items = me.items,
+ start = 0,
+ end = items.length - 1,
+ middle, comparison;
+ if (!sorterFn) {
+ sorterFn = me.generateComparator();
+ }
+ while (start <= end) {
+ middle = (start + end) >> 1;
+ comparison = sorterFn(newItem, items[middle]);
+ if (comparison >= 0) {
+ start = middle + 1;
+ } else if (comparison < 0) {
+ end = middle - 1;
+ }
+ }
+ return start;
+ },
+ /**
+ * @method reorder
+ * @inheritdoc Ext.util.AbstractMixedCollection#method-reorder
+ */
+ reorder: function(mapping) {
+ this.callParent([
+ mapping
+ ]);
+ this.fireEvent('sort', this);
+ },
+ /**
+ * Sorts this collection by key s.
+ * @param {"ASC"/"DESC"} [dir="ASC"] 'ASC' or 'DESC'. Defaults to 'ASC'.
+ * @param {Function} [fn] Comparison function that defines the sort order.
+ * Defaults to sorting by case insensitive string.
+ */
+ sortByKey: function(dir, fn) {
+ this._sort('key', dir, fn || function(a, b) {
+ var v1 = String(a).toUpperCase(),
+ v2 = String(b).toUpperCase();
+ return v1 > v2 ? 1 : (v1 < v2 ? -1 : 0);
+ });
+ }
+});
+
+/**
+ * Maintains an additional key map for an `Ext.util.Collection`. Instances of this class
+ * are seldom created manually. Rather they are created by the `Ext.util.Collection' when
+ * given an `extraKeys` config.
+ *
+ * @since 5.0.0
+ */
+Ext.define('Ext.util.CollectionKey', {
+ mixins: [
+ Ext.mixin.Identifiable
+ ],
+ isCollectionKey: true,
+ observerPriority: -200,
+ config: {
+ collection: null,
+ /**
+ * @cfg {Function/String} [keyFn]
+ * A function to retrieve the key of an item in the collection. This can be normal
+ * function that takes an item and returns the key or it can be the name of the
+ * method to call on an item to get the key.
+ *
+ * For example:
+ *
+ * new Ext.util.Collection({
+ * keys: {
+ * byName: {
+ * keyFn: 'getName' // each item has a "getName" method
+ * }
+ * }
+ * });
+ *
+ * Or equivalently:
+ *
+ * new Ext.util.Collection({
+ * keys: {
+ * byName: {
+ * keyFn: function (item) {
+ * return item.getName();
+ * }
+ * }
+ * }
+ * });
+ *
+ * @since 5.0.0
+ */
+ keyFn: null,
+ /**
+ * @cfg {String} property
+ * The name of the property on each item that is its key.
+ *
+ * new Ext.util.Collection({
+ * keys: {
+ * byName: 'name'
+ * }
+ * });
+ *
+ * Or equivalently:
+ *
+ * new Ext.util.Collection({
+ * keys: {
+ * byName: {
+ * property: 'name'
+ * }
+ * }
+ * });
+ *
+ * var item = collection.byName.get('fooname');
+ */
+ property: null,
+ /**
+ * @cfg {String} rootProperty
+ * The name of the sub-object property on each item that is its key. This value
+ * overrides `{@link Ext.util.Collection#rootProperty}`.
+ *
+ * new Ext.util.Collection({
+ * keys: {
+ * byName: {
+ * property: 'name',
+ * rootProperty: 'data'
+ * }
+ * }
+ * });
+ *
+ * var item = collection.byName.get('fooname');
+ */
+ rootProperty: null,
+ unique: true
+ },
+ /**
+ * This property is used to know when this `Index` is in sync with the `Collection`.
+ * When the two are synchronized, their `generation` values match.
+ * @private
+ * @readonly
+ * @since 5.0.0
+ */
+ generation: 0,
+ /**
+ * @property {Object} map
+ * An object used as map to get an object based on its key.
+ * @since 5.0.0
+ * @private
+ */
+ map: null,
+ /**
+ * @property {Number} mapRebuilds
+ * The number of times the `map` has been rebuilt. This is for diagnostic use.
+ * @private
+ * @readonly
+ */
+ mapRebuilds: 0,
+ /**
+ * @property {String} name
+ * This property is set by `Ext.util.Collection` when added via `extraKeys`.
+ * @readonly
+ */
+ constructor: function(config) {
+ this.initConfig(config);
+ if (!Ext.isFunction(this.getKey)) {
+ Ext.raise('CollectionKey requires a keyFn or property config');
+ }
+ },
+ /**
+ * Returns the item or, if not `unique` possibly array of items that have the given
+ * key.
+ * @param {Mixed} key The key that will match the `keyFn` return value or value of
+ * the specified `property`.
+ * @return {Object}
+ */
+ get: function(key) {
+ var map = this.map || this.getMap();
+ return map[key] || null;
+ },
+ /**
+ * @private
+ * Clears this index;
+ *
+ * Called by {@link Ext.util.Collection#clear} when the collection is cleared.
+ */
+ clear: function() {
+ this.map = null;
+ },
+ getRootProperty: function() {
+ var me = this,
+ root = this.callParent();
+ return root !== null ? root : me.getCollection().getRootProperty();
+ },
+ /**
+ * Returns the index of the item with the given key in the collection. If this is not
+ * a `unique` result, the index of the first item in the collection with the matching
+ * key.
+ *
+ * To iterate the indices of all items with a matching (not `unique`) key:
+ *
+ * for (index = collection.byName.indexOf('foo');
+ * index >= 0;
+ * index = collection.byName.indexOf('foo', index)) {
+ * // process item at "index"
+ * }
+ *
+ * @param {Mixed} key The key that will match the `keyFn` return value or value of
+ * the specified `property`.
+ * @param {Number} [startAt=-1] The index at which to start. Only occurrences beyond
+ * this index are returned.
+ * @return {Number} The index of the first item with the given `key` beyond the given
+ * `startAt` index or -1 if there are no such items.
+ */
+ indexOf: function(key, startAt) {
+ var map = this.map || this.getMap(),
+ item = map[key],
+ collection = this.getCollection(),
+ length = collection.length,
+ i, index, items, n;
+ if (!item) {
+ return -1;
+ }
+ if (startAt === undefined) {
+ startAt = -1;
+ }
+ if (item instanceof Array) {
+ items = item;
+ index = length;
+ // greater than any actual indexOf
+ for (n = items.length; n-- > 0; ) {
+ i = collection.indexOf(items[n]);
+ if (i < index && i > startAt) {
+ index = i;
+ }
+ }
+ if (index === length) {
+ return -1;
+ }
+ } else {
+ index = collection.indexOf(item);
+ }
+ return (index > startAt) ? index : -1;
+ },
+ /**
+ * Change the key for an existing item in the collection. If the old key does not
+ * exist this call does nothing.
+ * @param {Object} item The item whose key has changed.
+ * @param {String} oldKey The old key for the `item`.
+ * @since 5.0.0
+ */
+ updateKey: function(item, oldKey) {
+ var me = this,
+ map = me.map,
+ bucket, index;
+ if (map) {
+ bucket = map[oldKey];
+ if (bucket instanceof Array) {
+ index = Ext.Array.indexOf(bucket, item);
+ if (index >= 0) {
+ if (bucket.length > 2) {
+ bucket.splice(index, 1);
+ } else {
+ // If there is an array of 2 items, replace the array with the
+ // one remaining item. Since index then is either 0 or 1, the
+ // index of the other item is easy.
+ map[oldKey] = bucket[1 - index];
+ }
+ }
+ }
+ // "1 - 0" = 1, "1 - 1" = 0
+ else if (bucket) {
+ if (me.getUnique() && bucket !== item) {
+ Ext.raise('Incorrect oldKey "' + oldKey + '" for item with newKey "' + me.getKey(item) + '"');
+ }
+ delete map[oldKey];
+ }
+ me.add([
+ item
+ ]);
+ }
+ },
+ //-------------------------------------------------------------------------
+ // Calls from our Collection:
+ onCollectionAdd: function(collection, add) {
+ if (this.map) {
+ this.add(add.items);
+ }
+ },
+ onCollectionItemChange: function(collection, details) {
+ this.map = null;
+ },
+ onCollectionRefresh: function() {
+ this.map = null;
+ },
+ onCollectionRemove: function(collection, remove) {
+ var me = this,
+ map = me.map,
+ items = remove.items,
+ length = items.length,
+ i, key;
+ if (map) {
+ if (me.getUnique() && length < collection.length / 2) {
+ for (i = 0; i < length; ++i) {
+ key = me.getKey(items[i]);
+ delete map[key];
+ }
+ } else {
+ me.map = null;
+ }
+ }
+ },
+ //-------------------------------------------------------------------------
+ // Private
+ add: function(items) {
+ var me = this,
+ map = me.map,
+ bucket, i, item, key, length, unique;
+ length = items.length;
+ unique = me.getUnique();
+ for (i = 0; i < length; ++i) {
+ key = me.getKey(item = items[i]);
+ if (unique || !(key in map)) {
+ map[key] = item;
+ } else {
+ if (!((bucket = map[key]) instanceof Array)) {
+ map[key] = bucket = [
+ bucket
+ ];
+ }
+ bucket.push(item);
+ }
+ }
+ },
+ applyKeyFn: function(keyFn) {
+ if (Ext.isString(keyFn)) {
+ this.getKey = function(item) {
+ return item[keyFn]();
+ };
+ } else {
+ this.getKey = keyFn;
+ }
+ },
+ updateProperty: function(property) {
+ var root = this.getRootProperty();
+ this.getKey = function(item) {
+ return (root ? item[root] : item)[property];
+ };
+ },
+ getMap: function() {
+ var me = this,
+ map = me.map;
+ if (!map) {
+ me.map = map = {};
+ me.keysByItemKey = {};
+ ++me.mapRebuilds;
+ me.add(me.getCollection().items);
+ }
+ return map;
+ },
+ updateCollection: function(collection, oldCollection) {
+ if (collection) {
+ collection.addObserver(this);
+ }
+ if (oldCollection) {
+ oldCollection.removeObserver(this);
+ }
+ },
+ clone: function() {
+ return new Ext.util.CollectionKey(this.getCurrentConfig());
+ },
+ destroy: function() {
+ this.clear();
+ this.getCollection().removeObserver(this);
+ this.destroyed = true;
+ }
+});
+
+/**
+ * Represents a grouping of items. The grouper works in a similar fashion as the
+ * `Ext.util.Sorter` except that groups must be able to extract a value by which all items
+ * in the group can be collected. By default this is derived from the `property` config
+ * but can be customized using the `groupFn` if necessary.
+ *
+ * All items with the same group value compare as equal. If the group values do not compare
+ * equally, the sort can be controlled further by setting `sortProperty` or `sorterFn`.
+ */
+Ext.define('Ext.util.Grouper', {
+ extend: Ext.util.Sorter,
+ isGrouper: true,
+ config: {
+ /**
+ * @cfg {Function} groupFn This function is called for each item in the collection
+ * to determine the group to which it belongs. By default the `property` value is
+ * used to group items.
+ * @cfg {Object} groupFn.item The current item from the collection.
+ * @cfg {String} groupFn.return The group identifier for the item.
+ */
+ groupFn: null,
+ /**
+ * @cfg {String} property The field by which records are grouped. Groups are
+ * sorted alphabetically by group value as the default. To sort groups by a different
+ * property, use the {@link #sortProperty} configuration.
+ */
+ /**
+ * @cfg {String} sortProperty You can set this configuration if you want the groups
+ * to be sorted on something other then the group string returned by the `groupFn`.
+ * This serves the same role as `property` on a normal `Ext.util.Sorter`.
+ */
+ sortProperty: null
+ },
+ constructor: function(config) {
+ if (config) {
+ if (config.getGroupString) {
+ Ext.raise("Cannot set getGroupString - use groupFn instead");
+ }
+ }
+ this.callParent(arguments);
+ },
+ /**
+ * Returns the value for grouping to be used.
+ * @param {Ext.data.Model} item The Model instance
+ * @return {String}
+ */
+ getGroupString: function(item) {
+ var group = item.$collapsedGroupPlaceholder ? item.$groupKey : this._groupFn(item);
+ return (group != null) ? String(group) : '';
+ },
+ sortFn: function(item1, item2) {
+ var me = this,
+ lhs = me.getGroupString(item1),
+ rhs = me.getGroupString(item2),
+ property = me._sortProperty,
+ // Sorter's sortFn uses "_property"
+ root = me._root,
+ sorterFn = me._sorterFn,
+ transform = me._transform;
+ // Items with the same groupFn result must be equal... otherwise we sort them
+ // by sorterFn or sortProperty.
+ if (lhs === rhs) {
+ return 0;
+ }
+ if (property || sorterFn) {
+ if (sorterFn) {
+ return sorterFn.call(this, item1, item2);
+ }
+ if (root) {
+ item1 = item1[root];
+ item2 = item2[root];
+ }
+ lhs = item1[property];
+ rhs = item2[property];
+ if (transform) {
+ lhs = transform(lhs);
+ rhs = transform(rhs);
+ }
+ }
+ return (lhs > rhs) ? 1 : (lhs < rhs ? -1 : 0);
+ },
+ standardGroupFn: function(item) {
+ var root = this._root;
+ return (root ? item[root] : item)[this._property];
+ },
+ updateSorterFn: function() {},
+ // don't callParent here - we don't want to smash sortFn w/sorterFn
+ updateProperty: function() {
+ // we don't callParent since that is related to sorterFn smashing sortFn
+ if (!this.getGroupFn()) {
+ this.setGroupFn(this.standardGroupFn);
+ }
+ }
+});
+
+/**
+ * This class manages uniquely keyed objects such as {@link Ext.data.Model records} or
+ * {@link Ext.Component components}.
+ *
+ * ## Keys
+ *
+ * Unlike `Ext.util.MixedCollection` this class can only manage objects whose key can be
+ * extracted from the instance. That is, this class does not support "external" keys. This
+ * makes this class more efficient because it does not need to track keys in parallel with
+ * items. It also means key-to-item lookup will be optimal and never need to perform a
+ * linear search.
+ *
+ * ### Extra Keys
+ *
+ * In some cases items may need to be looked up by multiple property values. To enable this
+ * there is the `extraKeys` config.
+ *
+ * For example, to quickly look up items by their "name" property:
+ *
+ * var collection = new Ext.util.Collection({
+ * extraKeys: {
+ * byName: 'name' // based on "name" property of each item
+ * }
+ * });
+ *
+ * ## Ranges
+ *
+ * When methods accept index arguments to indicate a range of items, these are either an
+ * index and a number of items or a "begin" and "end" index.
+ *
+ * In the case of "begin" and "end", the "end" is the first item outside the range. This
+ * definition makes it simple to expression empty ranges because "length = end - begin".
+ *
+ * ### Negative Indices
+ *
+ * When an item index is provided, negative values are treated as offsets from the end of
+ * the collection. In other words the follow are equivalent:
+ *
+ * +---+---+---+---+---+---+
+ * | | | | | | |
+ * +---+---+---+---+---+---+
+ * 0 1 2 3 4 5
+ * -6 -5 -4 -3 -2 -1
+ *
+ * ## Legacy Classes
+ *
+ * The legacy classes `Ext.util.MixedCollection' and `Ext.util.AbstractMixedCollection`
+ * may be needed if external keys are required, but for all other situations this class
+ * should be used instead.
+ */
+Ext.define('Ext.util.Collection', {
+ mixins: [
+ Ext.mixin.Observable
+ ],
+ /**
+ * @property {Boolean} isCollection
+ * `true` in this class to identify an object as an instantiated Collection, or subclass
+ * thereof.
+ * @readonly
+ */
+ isCollection: true,
+ config: {
+ autoFilter: true,
+ /**
+ * @cfg {Boolean} [autoSort=true] `true` to maintain sorted order when items
+ * are added regardless of requested insertion point, or when an item mutation
+ * results in a new sort position.
+ *
+ * This does not affect a filtered Collection's reaction to mutations of the source
+ * Collection. If sorters are present when the source Collection is mutated,
+ * this Collection's sort order will always be maintained.
+ * @private
+ */
+ autoSort: true,
+ /**
+ * @cfg {Boolean} [autoGroup=true] `true` to sort by the grouper
+ * @private
+ */
+ autoGroup: true,
+ /**
+ * @cfg {Function} decoder
+ * A function that can convert newly added items to a proper type before being
+ * added to this collection.
+ */
+ decoder: null,
+ /**
+ * @cfg {Object} extraKeys
+ * One or more `Ext.util.CollectionKey` configuration objects or key properties.
+ * Each property of the given object is the name of the `CollectionKey` instance
+ * that is stored on this collection. The value of each property configures the
+ * `CollectionKey` instance.
+ *
+ * var collection = new Ext.util.Collection({
+ * extraKeys: {
+ * byName: 'name' // based on "name" property of each item
+ * }
+ * });
+ *
+ * Or equivalently:
+ *
+ * var collection = new Ext.util.Collection({
+ * extraKeys: {
+ * byName: {
+ * property: 'name'
+ * }
+ * }
+ * });
+ *
+ * To provide a custom key extraction function instead:
+ *
+ * var collection = new Ext.util.Collection({
+ * extraKeys: {
+ * byName: {
+ * keyFn: function (item) {
+ * return item.name;
+ * }
+ * }
+ * }
+ * });
+ *
+ * Or to call a key getter method from each item:
+ *
+ * var collection = new Ext.util.Collection({
+ * extraKeys: {
+ * byName: {
+ * keyFn: 'getName'
+ * }
+ * }
+ * });
+ *
+ * To use the above:
+ *
+ * var item = collection.byName.get('somename');
+ *
+ * **NOTE** Either a `property` or `keyFn` must be be specified to define each
+ * key.
+ * @since 5.0.0
+ */
+ extraKeys: null,
+ /**
+ * @cfg {Array/Ext.util.FilterCollection} filters
+ * The collection of {@link Ext.util.Filter Filters} for this collection. At the
+ * time a collection is created `filters` can be specified as a unit. After that
+ * time the normal `setFilters` method can also be given a set of replacement
+ * filters for the collection.
+ *
+ * Individual filters can be specified as an `Ext.util.Filter` instance, a config
+ * object for `Ext.util.Filter` or simply a function that will be wrapped in a
+ * instance with its {@link Ext.util.Filter#filterFn filterFn} set.
+ *
+ * For fine grain control of the filters collection, call `getFilters` to return
+ * the `Ext.util.Collection` instance that holds this collection's filters.
+ *
+ * var collection = new Ext.util.Collection();
+ * var filters = collection.getFilters(); // an Ext.util.FilterCollection
+ *
+ * function legalAge (item) {
+ * return item.age >= 21;
+ * }
+ *
+ * filters.add(legalAge);
+ *
+ * //...
+ *
+ * filters.remove(legalAge);
+ *
+ * Any changes to the `filters` collection will cause this collection to adjust
+ * its items accordingly (if `autoFilter` is `true`).
+ * @since 5.0.0
+ */
+ filters: null,
+ /**
+ * @cfg {Object} grouper
+ * A configuration object for this collection's {@link Ext.util.Grouper grouper}.
+ *
+ * For example, to group items by the first letter of the last name:
+ *
+ * var collection = new Ext.util.Collection({
+ * grouper: {
+ * groupFn: function (item) {
+ * return item.lastName.substring(0, 1);
+ * }
+ * }
+ * });
+ */
+ grouper: null,
+ /**
+ * @cfg {Ext.util.GroupCollection} groups
+ * The collection of to hold each group container. This collection is created and
+ * removed dynamically based on `grouper`. Application code should only need to
+ * call `getGroups` to retrieve the collection and not `setGroups`.
+ */
+ groups: null,
+ /**
+ * @cfg {Object} groupConfig
+ * A default configuration to be passed to any groups created by the
+ * {@link Ext.util.GroupCollection}. See {@link #groups}.
+ *
+ * @private
+ * @since 6.5.0
+ */
+ groupConfig: null,
+ /**
+ * @cfg {String} rootProperty
+ * The root property to use for aggregation, filtering and sorting. By default
+ * this is `null` but when containing things like {@link Ext.data.Model records}
+ * this config would likely be set to "data" so that property names are applied
+ * to the fields of each record.
+ */
+ rootProperty: null,
+ /**
+ * @cfg {Array/Ext.util.SorterCollection} sorters
+ * Array of {@link Ext.util.Sorter sorters} for this collection. At the time a
+ * collection is created the `sorters` can be specified as a unit. After that time
+ * the normal `setSorters` method can be also be given a set of replacement
+ * sorters.
+ *
+ * Individual sorters can be specified as an `Ext.util.Sorter` instance, a config
+ * object for `Ext.util.Sorter` or simply the name of a property by which to sort.
+ *
+ * An alternative way to extend the sorters is to call the `sort` method and pass
+ * a property or sorter config to add to the sorters.
+ *
+ * For fine grain control of the sorters collection, call `getSorters` to return
+ * the `Ext.util.Collection` instance that holds this collection's sorters.
+ *
+ * var collection = new Ext.util.Collection();
+ * var sorters = collection.getSorters(); // an Ext.util.SorterCollection
+ *
+ * sorters.add('name');
+ *
+ * //...
+ *
+ * sorters.remove('name');
+ *
+ * Any changes to the `sorters` collection will cause this collection to adjust
+ * its items accordingly (if `autoSort` is `true`).
+ *
+ * @since 5.0.0
+ */
+ sorters: null,
+ /**
+ * @cfg {Number} [multiSortLimit=3]
+ * The maximum number of sorters which may be applied to this Sortable when using
+ * the "multi" insertion position when adding sorters.
+ *
+ * New sorters added using the "multi" insertion position are inserted at the top
+ * of the sorters list becoming the new primary sort key.
+ *
+ * If the sorters collection has grown to longer then **`multiSortLimit`**, then
+ * the it is trimmed.
+ */
+ multiSortLimit: 3,
+ /**
+ * @cfg {String} defaultSortDirection
+ * The default sort direction to use if one is not specified.
+ */
+ defaultSortDirection: 'ASC',
+ /**
+ * @cfg {Ext.util.Collection} source
+ * The base `Collection`. This collection contains the items to which filters
+ * are applied to populate this collection. In this configuration, only the
+ * root `source` collection can have items truly added or removed.
+ * @since 5.0.0
+ */
+ source: null,
+ /**
+ * @cfg {Boolean} trackGroups
+ * `true` to track individual groups in a Ext.util.GroupCollection
+ * @private
+ */
+ trackGroups: true
+ },
+ /**
+ * @property {Number} generation
+ * Mutation counter which is incremented when the collection changes.
+ * @readonly
+ * @since 5.0.0
+ */
+ generation: 0,
+ /**
+ * @property {Object} indices
+ * An object used as map to get the index of an item.
+ * @private
+ * @since 5.0.0
+ */
+ indices: null,
+ /**
+ * @property {Number} indexRebuilds
+ * The number of times the `indices` have been rebuilt. This is for diagnostic use.
+ * @private
+ * @readonly
+ * @since 5.0.0
+ */
+ indexRebuilds: 0,
+ /**
+ * @property {Number} updating
+ * A counter that is increased by `beginUpdate` and decreased by `endUpdate`. When
+ * this transitions from 0 to 1 the `{@link #event-beginupdate beginupdate}` event is
+ * fired. When it transitions back from 1 to 0 the `{@link #event-endupdate endupdate}`
+ * event is fired.
+ * @readonly
+ * @since 5.0.0
+ */
+ updating: 0,
+ /**
+ * @property {Boolean} grouped
+ * A read-only flag indicating if this object is grouped.
+ * @readonly
+ */
+ grouped: false,
+ /**
+ * @property {Boolean} sorted
+ * A read-only flag indicating if this object is sorted. This flag may not be correct
+ * during an update of the sorter collection but will be correct before `onSortChange`
+ * is called. This flag is `true` if `grouped` is `true` because the collection is at
+ * least sorted by the `grouper`.
+ * @readonly
+ */
+ sorted: false,
+ /**
+ * @property {Boolean} filtered
+ * A read-only flag indicating if this object is filtered.
+ * @readonly
+ */
+ filtered: false,
+ /**
+ * @private
+ * Priority that is used for endupdate listeners on the filters and sorters.
+ * set to a very high priority so that our processing of these events takes place prior
+ * to user code - data must already be filtered/sorted when the user's handler runs
+ */
+ $endUpdatePriority: 1001,
+ /**
+ * @private
+ * `true` to destroy the sorter collection on destroy.
+ */
+ manageSorters: true,
+ /**
+ * @event add
+ * Fires after items have been added to the collection.
+ *
+ * All `{@link #event-add add}` and `{@link #event-remove remove}` events occur between
+ * `{@link #event-beginupdate beginupdate}` and `{@link #event-endupdate endupdate}`
+ * events so it is best to do only the minimal amount of work in response to these
+ * events and move the more expensive side-effects to an `endupdate` listener.
+ *
+ * @param {Ext.util.Collection} collection The collection being modified.
+ *
+ * @param {Object} details An object describing the addition.
+ *
+ * @param {Number} details.at The index in the collection where the add occurred.
+ *
+ * @param {Object} details.atItem The item after which the new items were inserted or
+ * `null` if at the beginning of the collection.
+ *
+ * @param {Object[]} details.items The items that are now added to the collection.
+ *
+ * @param {Array} [details.keys] If available this array holds the keys (extracted by
+ * `getKey`) for each item in the `items` array.
+ *
+ * @param {Object} [details.next] If more `{@link #event-add add}` events are in queue
+ * to be delivered this is a reference to the `details` instance for the next
+ * `{@link #event-add add}` event. This will only be the case when the collection is
+ * sorted as the new items often need to be inserted at multiple locations to maintain
+ * the sort. In this case, all of the new items have already been added not just those
+ * described by the first `{@link #event-add add}` event.
+ *
+ * @param {Object} [details.replaced] If this addition has a corresponding set of
+ * `{@link #event-remove remove}` events this reference holds the `details` object for
+ * the first `remove` event. That `details` object may have a `next` property if there
+ * are multiple associated `remove` events.
+ *
+ * @since 5.0.0
+ */
+ /**
+ * @event beginupdate
+ * Fired before changes are made to the collection. This event fires when the
+ * `beginUpdate` method is called and the counter it manages transitions from 0 to 1.
+ *
+ * All `{@link #event-add add}` and `{@link #event-remove remove}` events occur between
+ * `{@link #event-beginupdate beginupdate}` and `{@link #event-endupdate endupdate}`
+ * events so it is best to do only the minimal amount of work in response to these
+ * events and move the more expensive side-effects to an `endupdate` listener.
+ *
+ * @param {Ext.util.Collection} collection The collection being modified.
+ *
+ * @since 5.0.0
+ */
+ /**
+ * @event endupdate
+ * Fired after changes are made to the collection. This event fires when the `endUpdate`
+ * method is called and the counter it manages transitions from 1 to 0.
+ *
+ * All `{@link #event-add add}` and `{@link #event-remove remove}` events occur between
+ * `{@link #event-beginupdate beginupdate}` and `{@link #event-endupdate endupdate}`
+ * events so it is best to do only the minimal amount of work in response to these
+ * events and move the more expensive side-effects to an `endupdate` listener.
+ *
+ * @param {Ext.util.Collection} collection The collection being modified.
+ *
+ * @since 5.0.0
+ */
+ /**
+ * @event beforeitemchange
+ * This event fires before an item change is reflected in the collection. This event
+ * is always followed by an `itemchange` event and, depending on the change, possibly
+ * an `add`, `remove` and/or `updatekey` event.
+ *
+ * @param {Ext.util.Collection} collection The collection being modified.
+ *
+ * @param {Object} details An object describing the change.
+ *
+ * @param {Object} details.item The item that has changed.
+ *
+ * @param {String} details.key The key of the item that has changed.
+ *
+ * @param {Boolean} details.filterChanged This is `true` if the filter status of the
+ * `item` has changed. That is, the item was previously filtered out and is no longer
+ * or the opposite.
+ *
+ * @param {Ext.util.Group} details.group The group containing the `item`. **(since 6.5.1)**
+ *
+ * @param {Boolean} details.groupChanged This is `true` if the item is moving between
+ * groups. See also the `group` and `oldGroup` properties. **(since 6.5.1)**
+ *
+ * @param {Boolean} details.keyChanged This is `true` if the item has changed keys. If
+ * so, check `oldKey` for the old key. An `updatekey` event will follow.
+ *
+ * @param {Boolean} details.indexChanged This is `true` if the item needs to move to
+ * a new index in the collection due to sorting. The index can be seen in `index`.
+ * The old index is in `oldIndex`.
+ *
+ * @param {String[]} [details.modified] If known this property holds the array of names
+ * of the modified properties of the item.
+ *
+ * @param {Boolean} [details.filtered] This value is `true` if the item will be filtered
+ * out of the collection.
+ *
+ * @param {Number} [details.index] The new index in the collection for the item if
+ * the item is being moved (see `indexChanged`). If the item is being removed due to
+ * filtering, this will be -1.
+ *
+ * @param {Ext.util.Group} details.oldGroup The group that previously contained the
+ * `item`. **(since 6.5.1)**
+ *
+ * @param {Number} [details.oldIndex] The old index in the collection for the item if
+ * the item is being moved (see `indexChanged`). If the item was being removed due to
+ * filtering, this will be -1.
+ *
+ * @param {Object} [details.oldKey] The old key for the `item` if the item's key has
+ * changed (see `keyChanged`).
+ *
+ * @param {Boolean} [details.wasFiltered] This value is `true` if the item was filtered
+ * out of the collection.
+ *
+ * @since 5.0.0
+ */
+ /**
+ * @event itemchange
+ * This event fires after an item change is reflected in the collection. This event
+ * always follows a `beforeitemchange` event and its corresponding `add`, `remove`
+ * and/or `updatekey` events.
+ *
+ * @param {Ext.util.Collection} collection The collection being modified.
+ *
+ * @param {Object} details An object describing the change.
+ *
+ * @param {Object} details.item The item that has changed.
+ *
+ * @param {String} details.key The key of the item that has changed.
+ *
+ * @param {Boolean} details.filterChanged This is `true` if the filter status of the
+ * `item` has changed. That is, the item was previously filtered out and is no longer
+ * or the opposite.
+ *
+ * @param {Ext.util.Group} details.group The group containing the `item`. **(since 6.5.1)**
+ *
+ * @param {Boolean} details.groupChanged This is `true` if the item is moving between
+ * groups. See also the `group` and `oldGroup` properties. **(since 6.5.1)**
+ *
+ * @param {Object} details.keyChanged This is `true` if the item has changed keys. If
+ * so, check `oldKey` for the old key. An `updatekey` event will have been sent.
+ *
+ * @param {Boolean} details.indexChanged This is `true` if the item was moved to a
+ * new index in the collection due to sorting. The index can be seen in `index`.
+ * The old index is in `oldIndex`.
+ *
+ * @param {String[]} [details.modified] If known this property holds the array of names
+ * of the modified properties of the item.
+ *
+ * @param {Boolean} [details.filtered] This value is `true` if the item is filtered
+ * out of the collection.
+ *
+ * @param {Number} [details.index] The new index in the collection for the item if
+ * the item has been moved (see `indexChanged`). If the item is removed due to
+ * filtering, this will be -1.
+ *
+ * @param {Ext.util.Group} details.oldGroup The group that previously contained the
+ * `item`. **(since 6.5.1)**
+ *
+ * @param {Number} [details.oldIndex] The old index in the collection for the item if
+ * the item has been moved (see `indexChanged`). If the item was being removed due to
+ * filtering, this will be -1.
+ *
+ * @param {Object} [details.oldKey] The old key for the `item` if the item's key has
+ * changed (see `keyChanged`).
+ *
+ * @param {Boolean} [details.wasFiltered] This value is `true` if the item was filtered
+ * out of the collection.
+ *
+ * @since 5.0.0
+ */
+ /**
+ * @event refresh
+ * This event fires when the collection has changed entirely. This event is fired in
+ * cases where the collection's filter is updated or the items are sorted. While the
+ * items previously in the collection may remain the same, the order at a minimum has
+ * changed in ways that cannot be simply translated to other events.
+ *
+ * @param {Ext.util.Collection} collection The collection being modified.
+ */
+ /**
+ * @event remove
+ * Fires after items have been removed from the collection. Some properties of this
+ * object may not be present if calculating them is deemed too expensive. These are
+ * marked as "optional".
+ *
+ * All `{@link #event-add add}` and `{@link #event-remove remove}` events occur between
+ * `{@link #event-beginupdate beginupdate}` and `{@link #event-endupdate endupdate}`
+ * events so it is best to do only the minimal amount of work in response to these
+ * events and move the more expensive side-effects to an `endupdate` listener.
+ *
+ * @param {Ext.util.Collection} collection The collection being modified.
+ *
+ * @param {Object} details An object describing the removal.
+ *
+ * @param {Number} details.at The index in the collection where the removal occurred.
+ *
+ * @param {Object[]} details.items The items that are now removed from the collection.
+ *
+ * @param {Array} [details.keys] If available this array holds the keys (extracted by
+ * `getKey`) for each item in the `items` array.
+ *
+ * @param {Object} [details.map] If available this is a map keyed by the key of each
+ * item in the `items` array. This will often contain all of the items being removed
+ * and not just the items in the range described by this event. The value held in this
+ * map is the item.
+ *
+ * @param {Object} [details.next] If more `{@link #event-remove remove}` events are in
+ * queue to be delivered this is a reference to the `details` instance for the next
+ * remove event.
+ *
+ * @param {Object} [details.replacement] If this removal has a corresponding
+ * `{@link #event-add add}` taking place this reference holds the `details` object for
+ * that `add` event. If the collection is sorted, the new items are pre-sorted but the
+ * `at` property for the `replacement` will **not** be correct. The new items will be
+ * added in one or more chunks at their proper index.
+ *
+ * @since 5.0.0
+ */
+ /**
+ * @event sort
+ * This event fires after the contents of the collection have been sorted.
+ *
+ * @param {Ext.util.Collection} collection The collection being sorted.
+ */
+ /**
+ * @event beforesort
+ * @private
+ * This event fires before the contents of the collection have been sorted.
+ *
+ * @param {Ext.util.Collection} collection The collection being sorted.
+ * @param {Ext.util.Sorter[]} sorters Array of sorters applied to the Collection.
+ */
+ /**
+ * @event updatekey
+ * Fires after the key for an item has changed.
+ *
+ * @param {Ext.util.Collection} collection The collection being modified.
+ *
+ * @param {Object} details An object describing the update.
+ *
+ * @param {Object} details.item The item whose key has changed.
+ *
+ * @param {Object} details.newKey The new key for the `item`.
+ *
+ * @param {Object} details.oldKey The old key for the `item`.
+ *
+ * @since 5.0.0
+ */
+ constructor: function(config) {
+ var me = this;
+ me.callParent([
+ config
+ ]);
+ /**
+ * @property {Object[]} items
+ * An array containing the items.
+ * @private
+ * @since 5.0.0
+ */
+ me.items = [];
+ /**
+ * @property {Object} map
+ * An object used as a map to find items based on their key.
+ * @private
+ * @since 5.0.0
+ */
+ me.map = {};
+ /**
+ * @property {Number} length
+ * The count of items in the collection.
+ * @readonly
+ * @since 5.0.0
+ */
+ me.length = 0;
+ /**
+ * @cfg {Function} [keyFn]
+ * A function to retrieve the key of an item in the collection. If provided,
+ * this replaces the default `getKey` method. The default `getKey` method handles
+ * items that have either an "id" or "_id" property or failing that a `getId`
+ * method to call.
+ * @since 5.0.0
+ */
+ if (config && config.keyFn) {
+ me.getKey = config.keyFn;
+ }
+ me.mixins.observable.constructor.call(me, config);
+ },
+ /**
+ * Destroys this collection. This is only necessary if this collection uses a `source`
+ * collection as that relationship will keep a reference from the `source` to this
+ * collection and potentially leak memory.
+ * @since 5.0.0
+ */
+ destroy: function() {
+ var me = this,
+ filters = me._filters,
+ sorters = me._sorters,
+ groups = me._groups;
+ if (filters) {
+ filters.destroy();
+ me._filters = null;
+ }
+ if (sorters) {
+ // Set to false here so updateSorters doesn't trigger
+ // the template methods
+ me.grouped = me.sorted = false;
+ me.setSorters(null);
+ if (me.manageSorters) {
+ sorters.destroy();
+ }
+ }
+ if (groups) {
+ groups.destroy();
+ me._groups = null;
+ }
+ me.setSource(null);
+ me.observers = me.items = me.map = null;
+ me.callParent();
+ },
+ /**
+ * Adds an item to the collection. If the item already exists or an item with the
+ * same key exists, the old item will be removed and the new item will be added to
+ * the end.
+ *
+ * This method also accepts an array of items or simply multiple items as individual
+ * arguments. The following 3 code sequences have the same end result:
+ *
+ * // Call add() once per item (not optimal - best avoided):
+ * collection.add(itemA);
+ * collection.add(itemB);
+ * collection.add(itemC);
+ * collection.add(itemD);
+ *
+ * // Call add() with each item as an argument:
+ * collection.add(itemA, itemB, itemC, itemD);
+ *
+ * // Call add() with the items as an array:
+ * collection.add([ itemA, itemB, itemC, itemD ]);
+ *
+ * The first form should be avoided where possible because the collection and all
+ * parties "watching" it will be updated 4 times.
+ *
+ * @param {Object/Object[]} item The item or items to add.
+ * @return {Object/Object[]} The item or items added.
+ * @since 5.0.0
+ */
+ add: function(item) {
+ var me = this,
+ items = me.decodeItems(arguments, 0),
+ ret = items;
+ if (items.length) {
+ me.splice(me.length, 0, items);
+ ret = (items.length === 1) ? items[0] : items;
+ }
+ return ret;
+ },
+ /**
+ * Adds an item to the collection while removing any existing items.
+ * Similar to {@link #method-add}.
+ * @param {Object/Object[]} item The item or items to add.
+ * @return {Object/Object[]} The item or items added.
+ * @since 5.0.0
+ */
+ replaceAll: function() {
+ var me = this,
+ ret, items;
+ items = me.decodeItems(arguments, 0);
+ ret = items;
+ if (items.length) {
+ me.splice(0, me.length, items);
+ ret = (items.length === 1) ? items[0] : items;
+ } else {
+ me.removeAll();
+ }
+ return ret;
+ },
+ /**
+ * Returns the result of the specified aggregation operation against all items in this
+ * collection.
+ *
+ * This method is not typically called directly because there are convenience methods
+ * for each of the supported `operation` values. These are:
+ *
+ * * **average** - Returns the average value.
+ * * **bounds** - Returns an array of `[min, max]`.
+ * * **max** - Returns the maximum value or `undefined` if empty.
+ * * **min** - Returns the minimum value or `undefined` if empty.
+ * * **sum** - Returns the sum of all values.
+ *
+ * For example:
+ *
+ * result = collection.aggregate('age', 'sum');
+ *
+ * result = collection.aggregate('age', 'sum', 2, 10); // the 8 items at index 2
+ *
+ * To provide a custom operation function:
+ *
+ * function averageAgeOfMinors (items, values) {
+ * var sum = 0,
+ * count = 0;
+ *
+ * for (var i = 0; i < values.length; ++i) {
+ * if (values[i] < 18) {
+ * sum += values[i];
+ * ++count;
+ * }
+ * }
+ *
+ * return count ? sum / count : 0;
+ * }
+ *
+ * result = collection.aggregate('age', averageAgeOfMinors);
+ *
+ * @param {String} property The name of the property to aggregate from each item.
+ * @param {String/Function} operation The operation to perform.
+ * @param {Array} operation.items The items on which the `operation` function is to
+ * operate.
+ * @param {Array} operation.values The values on which the `operation` function is to
+ * operate.
+ * @param {Number} [begin] The index of the first item in `items` to include in the
+ * aggregation.
+ * @param {Number} [end] The index at which to stop aggregating `items`. The item at
+ * this index will *not* be included in the aggregation.
+ * @param {Object} [scope] The `this` pointer to use if `operation` is a function.
+ * Defaults to this collection.
+ * @return {Object}
+ */
+ aggregate: function(property, operation, begin, end, scope) {
+ var me = this,
+ args = Ext.Array.slice(arguments);
+ args.unshift(me.items);
+ return me.aggregateItems.apply(me, args);
+ },
+ /**
+ * See {@link #aggregate}. The functionality is the same, however the aggregates are
+ * provided per group. Assumes this collection has an active {@link #grouper}.
+ *
+ * @param {String} property The name of the property to aggregate from each item.
+ * @param {String/Function} operation The operation to perform.
+ * @param {Array} operation.items The items on which the `operation` function is to
+ * operate.
+ * @param {Array} operation.values The values on which the `operation` function is to
+ * operate.
+ * @param {Object} [scope] The `this` pointer to use if `operation` is a function.
+ * Defaults to this collection.
+ * @return {Object}
+ */
+ aggregateByGroup: function(property, operation, scope) {
+ var groups = this.getGroups();
+ return this.aggregateGroups(groups, property, operation, scope);
+ },
+ /**
+ * Returns the result of the specified aggregation operation against the given items.
+ * For details see `aggregate`.
+ *
+ * @param {Array} items The items to aggregate.
+ * @param {String} property The name of the property to aggregate from each item.
+ * @param {String/Function} operation The operation to perform.
+ * @param {Array} operation.items The items on which the `operation` function is to
+ * operate.
+ * @param {Array} operation.values The values on which the `operation` function is to
+ * operate.
+ * @param {Number} [begin] The index of the first item in `items` to include in the
+ * aggregation.
+ * @param {Number} [end] The index at which to stop aggregating `items`. The item at
+ * this index will *not* be included in the aggregation.
+ * @param {Object} [scope] The `this` pointer to use if `operation` is a function.
+ * Defaults to this collection.
+ *
+ * @private
+ * @return {Object}
+ */
+ aggregateItems: function(items, property, operation, begin, end, scope) {
+ var me = this,
+ range = Ext.Number.clipIndices(items.length, [
+ begin,
+ end
+ ]),
+ // Only extract items into new array if a subset is required
+ subsetRequested = (begin !== 0 && end !== items.length),
+ i, j, rangeLen, root, value, values, valueItems;
+ begin = range[0];
+ end = range[1];
+ if (!Ext.isFunction(operation)) {
+ operation = me._aggregators[operation];
+ return operation.call(me, items, begin, end, property, me.getRootProperty());
+ }
+ root = me.getRootProperty();
+ // Preallocate values array with known set size.
+ // valueItems can be just the items array is a subset has not been requested
+ values = new Array(rangeLen);
+ valueItems = subsetRequested ? new Array(rangeLen) : items;
+ // Collect the extracted property values and the items for passing to the operation.
+ for (i = begin , j = 0; i < end; ++i , j++) {
+ if (subsetRequested) {
+ valueItems[j] = value = items[i];
+ }
+ values[j] = (root ? value[root] : value)[property];
+ }
+ return operation.call(scope || me, items, values, 0);
+ },
+ /**
+ * Aggregates a set of groups.
+ * @param {Ext.util.GroupCollection} groups The groups
+ * @param {String} property The name of the property to aggregate from each item.
+ * @param {String/Function} operation The operation to perform.
+ * @param {Array} operation.values The values on which the `operation` function is to
+ * operate.
+ * @param {Array} operation.items The items on which the `operation` function is to
+ * operate.
+ * @param {Number} operation.index The index in `items` at which the `operation`
+ * function is to start. The `values.length` indicates the number of items involved.
+ * @param {Object} [scope] The `this` pointer to use if `operation` is a function.
+ * Defaults to this collection.
+ *
+ * @return {Object}
+ * @private
+ */
+ aggregateGroups: function(groups, property, operation, scope) {
+ var items = groups.items,
+ len = items.length,
+ callDirect = !Ext.isFunction(operation),
+ out = {},
+ i, group, result;
+ for (i = 0; i < len; ++i) {
+ group = items[i];
+ if (!callDirect) {
+ result = this.aggregateItems(group.items, property, operation, null, null, scope);
+ } else {
+ result = group[operation](property);
+ }
+ out[group.getGroupKey()] = result;
+ }
+ return out;
+ },
+ /**
+ * This method is called to indicate the start of multiple changes to the collection.
+ * Application code should seldom need to call this method as it is called internally
+ * when needed. If multiple collection changes are needed, consider wrapping them in
+ * an `update` call rather than calling `beginUpdate` directly.
+ *
+ * Internally this method increments a counter that is decremented by `endUpdate`. It
+ * is important, therefore, that if you call `beginUpdate` directly you match that
+ * call with a call to `endUpdate` or you will prevent the collection from updating
+ * properly.
+ *
+ * For example:
+ *
+ * var collection = new Ext.util.Collection();
+ *
+ * collection.beginUpdate();
+ *
+ * collection.add(item);
+ * // ...
+ *
+ * collection.insert(index, otherItem);
+ * //...
+ *
+ * collection.endUpdate();
+ *
+ * @since 5.0.0
+ */
+ beginUpdate: function() {
+ if (!this.updating++) {
+ // jshint ignore:line
+ this.notify('beginupdate');
+ }
+ },
+ /**
+ * Removes all items from the collection. This is similar to `removeAll` except that
+ * `removeAll` fire events to inform listeners. This means that this method should be
+ * called only when you are sure there are no listeners.
+ * @since 5.0.0
+ */
+ clear: function() {
+ var me = this,
+ generation = me.generation,
+ ret = generation ? me.items : [],
+ extraKeys, indexName;
+ if (generation) {
+ me.items.length = me.length = 0;
+ me.map = {};
+ me.indices = {};
+ me.generation++;
+ // Clear any extraKey indices associated with this Collection
+ extraKeys = me.getExtraKeys();
+ if (extraKeys) {
+ for (indexName in extraKeys) {
+ extraKeys[indexName].clear();
+ }
+ }
+ }
+ return ret;
+ },
+ /**
+ * Creates a shallow copy of this collection
+ * @return {Ext.util.Collection}
+ * @since 5.0.0
+ */
+ clone: function() {
+ var me = this,
+ copy = new me.self(me.initialConfig);
+ copy.add(me.items);
+ return copy;
+ },
+ /**
+ * Collects unique values of a particular property in this Collection.
+ * @param {String} property The property to collect on
+ * @param {String} root (optional) 'root' property to extract the first argument from.
+ * This is used mainly when summing fields in records, where the fields are all stored
+ * inside the 'data' object
+ * @param {Boolean} [allowNull] Pass `true` to include `null`, `undefined` or empty
+ * string values.
+ * @return {Array} The unique values
+ * @since 5.0.0
+ */
+ collect: function(property, root, allowNull) {
+ var items = this.items,
+ length = items.length,
+ map = {},
+ ret = [],
+ i, strValue, value;
+ for (i = 0; i < length; ++i) {
+ value = items[i];
+ value = (root ? value[root] : value)[property];
+ strValue = String(value);
+ if ((allowNull || !Ext.isEmpty(value)) && !map[strValue]) {
+ map[strValue] = 1;
+ ret.push(value);
+ }
+ }
+ return ret;
+ },
+ /**
+ * Returns true if the collection contains the passed Object as an item.
+ * @param {Object} item The item to look for in the collection.
+ * @return {Boolean} `true` if the collection contains the item.
+ * @since 5.0.0
+ */
+ contains: function(item) {
+ var ret = false,
+ key;
+ if (item != null) {
+ key = this.getKey(item);
+ ret = this.map[key] === item;
+ }
+ return ret;
+ },
+ /**
+ * Returns true if the collection contains all the passed items. If the first argument
+ * is an array, then the items in that array are checked. Otherwise, all arguments
+ * passed to this method are checked.
+ *
+ * @param {Object.../Object[]} items The item(s) that must be in the collection.
+ * @return {Boolean} `true` if the collection contains all the items.
+ * @since 6.5.2
+ */
+ containsAll: function(items) {
+ var all = Ext.isArray(items) ? items : arguments,
+ i;
+ for (i = all.length; i-- > 0; ) {
+ if (!this.contains(all[i])) {
+ return false;
+ }
+ }
+ return true;
+ },
+ /**
+ * Returns true if the collection contains the passed Object as a key.
+ * @param {String} key The key to look for in the collection.
+ * @return {Boolean} True if the collection contains the Object as a key.
+ * @since 5.0.0
+ */
+ containsKey: function(key) {
+ return key in this.map;
+ },
+ /**
+ * Creates a new collection that is a filtered subset of this collection. The filter
+ * passed can be a function, a simple property name and value, an `Ext.util.Filter`
+ * instance, an array of `Ext.util.Filter` instances.
+ *
+ * If the passed filter is a function the second argument is its "scope" (or "this"
+ * pointer). The function should return `true` given each item in the collection if
+ * that item should be included in the filtered collection.
+ *
+ * var people = new Ext.util.Collection();
+ *
+ * people.add([
+ * { id: 1, age: 25, name: 'Ed' },
+ * { id: 2, age: 24, name: 'Tommy' },
+ * { id: 3, age: 24, name: 'Arne' },
+ * { id: 4, age: 26, name: 'Aaron' }
+ * ]);
+ *
+ * // Create a collection of people who are older than 24:
+ * var oldPeople = people.createFiltered(function (item) {
+ * return item.age > 24;
+ * });
+ *
+ * If the passed filter is a `Ext.util.Filter` instance or array of `Ext.util.Filter`
+ * instances the filter(s) are used to produce the filtered collection and there are
+ * no further arguments.
+ *
+ * If the passed filter is a string it is understood as the name of the property by
+ * which to filter. The second argument is the "value" used to compare each item's
+ * property value. This comparison can be further tuned with the `anyMatch` and
+ * `caseSensitive` (optional) arguments.
+ *
+ * // Create a new Collection containing only the items where age == 24
+ * var middleAged = people.createFiltered('age', 24);
+ *
+ * Alternatively you can apply `filters` to this Collection by calling `setFilters`
+ * or modifying the filter collection returned by `getFilters`.
+ *
+ * @param {Ext.util.Filter[]/String/Function} property A property on your objects, an
+ * array of {@link Ext.util.Filter Filter} objects or a filter function.
+ *
+ * @param {Object} value If `property` is a function, this argument is the "scope"
+ * (or "this" pointer) for the function. Otherwise this is either a `RegExp` to test
+ * property values or the value with which to compare.
+ *
+ * @param {Boolean} [anyMatch=false] True to match any part of the string, not just
+ * the beginning.
+ *
+ * @param {Boolean} [caseSensitive=false] True for case sensitive comparison.
+ *
+ * @param {Boolean} [exactMatch=false] `true` to force exact match (^ and $ characters
+ * added to the regex).
+ *
+ * @return {Ext.util.Collection} The new, filtered collection.
+ *
+ * @since 5.0.0
+ */
+ createFiltered: function(property, value, anyMatch, caseSensitive, exactMatch) {
+ var me = this,
+ ret = new me.self(Ext.applyIf({
+ source: null
+ }, me.initialConfig)),
+ root = me.getRootProperty(),
+ items = me.items,
+ length, i, filters, fn, scope;
+ if (Ext.isFunction(property)) {
+ fn = property;
+ scope = value;
+ } else {
+ // support for the simple case of filtering by property/value
+ if (Ext.isString(property)) {
+ filters = [
+ new Ext.util.Filter({
+ property: property,
+ value: value,
+ root: root,
+ anyMatch: anyMatch,
+ caseSensitive: caseSensitive,
+ exactMatch: exactMatch
+ })
+ ];
+ } else if (property instanceof Ext.util.Filter) {
+ filters = [
+ property
+ ];
+ property.setRoot(root);
+ } else if (Ext.isArray(property)) {
+ filters = property.slice(0);
+ for (i = 0 , length = filters.length; i < length; ++i) {
+ filters[i].setRoot(root);
+ }
+ }
+ // At this point we have an array of zero or more Ext.util.Filter objects to
+ // filter with, so here we construct a function that combines these filters by
+ // ANDing them together and filter by that.
+ fn = Ext.util.Filter.createFilterFn(filters);
+ }
+ scope = scope || me;
+ for (i = 0 , length = items.length; i < length; i++) {
+ if (fn.call(scope, items[i])) {
+ ret.add(items[i]);
+ }
+ }
+ return ret;
+ },
+ /**
+ * Filter by a function. Returns a new collection that has been filtered.
+ * The passed function will be called with each object in the collection.
+ * If the function returns true, the value is included otherwise it is filtered.
+ * @param {Function} fn The function to be called.
+ * @param {Mixed} fn.item The collection item.
+ * @param {String} fn.key The key of collection item.
+ * @param {Object} scope (optional) The scope (this
reference) in
+ * which the function is executed. Defaults to this Collection.
+ * @return {Ext.util.Collection} The new filtered collection
+ * @deprecated 5.0.0 This method is deprecated.
+ */
+ filterBy: function(fn, scope) {
+ return this.createFiltered(fn, scope);
+ },
+ /**
+ * Executes the specified function once for every item in the collection. If the value
+ * returned by `fn` is `false` the iteration stops. In all cases, the last value that
+ * `fn` returns is returned by this method.
+ *
+ * @param {Function} fn The function to execute for each item.
+ * @param {Object} fn.item The collection item.
+ * @param {Number} fn.index The index of item.
+ * @param {Number} fn.len Total length of collection.
+ * @param {Object} [scope=this] The scope (`this` reference) in which the function
+ * is executed. Defaults to this collection.
+ * @since 5.0.0
+ */
+ each: function(fn, scope) {
+ var items = this.items,
+ len = items.length,
+ i, ret;
+ if (len) {
+ scope = scope || this;
+ items = items.slice(0);
+ // safe for re-entrant calls
+ for (i = 0; i < len; i++) {
+ ret = fn.call(scope, items[i], i, len);
+ if (ret === false) {
+ break;
+ }
+ }
+ }
+ return ret;
+ },
+ /**
+ * Executes the specified function once for every key in the collection, passing each
+ * key, and its associated item as the first two parameters. If the value returned by
+ * `fn` is `false` the iteration stops. In all cases, the last value that `fn` returns
+ * is returned by this method.
+ *
+ * @param {Function} fn The function to execute for each item.
+ * @param {String} fn.key The key of collection item.
+ * @param {Object} fn.item The collection item.
+ * @param {Number} fn.index The index of item.
+ * @param {Number} fn.len Total length of collection.
+ * @param {Object} [scope=this] The scope (`this` reference) in which the function
+ * is executed. Defaults to this collection.
+ * @since 5.0.0
+ */
+ eachKey: function(fn, scope) {
+ var me = this,
+ items = me.items,
+ len = items.length,
+ i, item, key, ret;
+ if (len) {
+ scope = scope || me;
+ items = items.slice(0);
+ // safe for re-entrant calls
+ for (i = 0; i < len; i++) {
+ key = me.getKey(item = items[i]);
+ ret = fn.call(scope, key, item, i, len);
+ if (ret === false) {
+ break;
+ }
+ }
+ }
+ return ret;
+ },
+ /**
+ * This method is called after modifications are complete on a collection. For details
+ * see `beginUpdate`.
+ * @since 5.0.0
+ */
+ endUpdate: function() {
+ if (!--this.updating) {
+ this.notify('endupdate');
+ }
+ },
+ /**
+ * Finds the first matching object in this collection by a specific property/value.
+ *
+ * @param {String} property The name of a property on your objects.
+ * @param {String/RegExp} value A string that the property values
+ * should start with or a RegExp to test against the property.
+ * @param {Number} [start=0] The index to start searching at.
+ * @param {Boolean} [startsWith=true] Pass `false` to allow a match start anywhere in
+ * the string. By default the `value` will match only at the start of the string.
+ * @param {Boolean} [endsWith=true] Pass `false` to allow the match to end before the
+ * end of the string. By default the `value` will match only at the end of the string.
+ * @param {Boolean} [ignoreCase=true] Pass `false` to make the `RegExp` case
+ * sensitive (removes the 'i' flag).
+ * @return {Object} The first item in the collection which matches the criteria or
+ * `null` if none was found.
+ * @since 5.0.0
+ */
+ find: function(property, value, start, startsWith, endsWith, ignoreCase) {
+ var regex, root;
+ if (Ext.isEmpty(value, false)) {
+ return null;
+ }
+ regex = Ext.String.createRegex(value, startsWith, endsWith, ignoreCase);
+ root = this.getRootProperty();
+ return this.findBy(function(item) {
+ return item && regex.test((root ? item[root] : item)[property]);
+ }, null, start);
+ },
+ /**
+ * Returns the first item in the collection which elicits a true return value from the
+ * passed selection function.
+ * @param {Function} fn The selection function to execute for each item.
+ * @param {Object} fn.item The collection item.
+ * @param {String} fn.key The key of collection item.
+ * @param {Object} [scope=this] The scope (`this` reference) in which the function
+ * is executed. Defaults to this collection.
+ * @param {Number} [start=0] The index at which to start searching.
+ * @return {Object} The first item in the collection which returned true from the selection
+ * function, or null if none was found.
+ * @since 5.0.0
+ */
+ findBy: function(fn, scope, start) {
+ var me = this,
+ items = me.items,
+ len = items.length,
+ i, item, key;
+ scope = scope || me;
+ for (i = start || 0; i < len; i++) {
+ key = me.getKey(item = items[i]);
+ if (fn.call(scope, item, key)) {
+ return items[i];
+ }
+ }
+ return null;
+ },
+ /**
+ * Finds the index of the first matching object in this collection by a specific
+ * property/value.
+ *
+ * @param {String} property The name of a property on your objects.
+ * @param {String/RegExp} value A string that the property values
+ * should start with or a RegExp to test against the property.
+ * @param {Number} [start=0] The index to start searching at.
+ * @param {Boolean} [startsWith=true] Pass `false` to allow a match start anywhere in
+ * the string. By default the `value` will match only at the start of the string.
+ * @param {Boolean} [endsWith=true] Pass `false` to allow the match to end before the
+ * end of the string. By default the `value` will match only at the end of the string.
+ * @param {Boolean} [ignoreCase=true] Pass `false` to make the `RegExp` case
+ * sensitive (removes the 'i' flag).
+ * @return {Number} The matched index or -1 if not found.
+ * @since 5.0.0
+ */
+ findIndex: function(property, value, start, startsWith, endsWith, ignoreCase) {
+ var item = this.find(property, value, start, startsWith, endsWith, ignoreCase);
+ return item ? this.indexOf(item) : -1;
+ },
+ /**
+ * Find the index of the first matching object in this collection by a function.
+ * If the function returns `true` it is considered a match.
+ * @param {Function} fn The function to be called.
+ * @param {Object} fn.item The collection item.
+ * @param {String} fn.key The key of collection item.
+ * @param {Object} [scope=this] The scope (`this` reference) in which the function
+ * is executed. Defaults to this collection.
+ * @param {Number} [start=0] The index at which to start searching.
+ * @return {Number} The matched index or -1
+ * @since 5.0.0
+ */
+ findIndexBy: function(fn, scope, start) {
+ var item = this.findBy(fn, scope, start);
+ return item ? this.indexOf(item) : -1;
+ },
+ /**
+ * Returns the first item in the collection.
+ * @param {Boolean} [grouped] `true` to extract the first item in each group. Only applies if
+ * a {@link #grouper} is active in the collection.
+ * @return {Object} The first item in the collection. If the grouped parameter is passed,
+ * see {@link #aggregateByGroup} for information on the return type.
+ * @since 5.0.0
+ */
+ first: function(grouped) {
+ var groups = grouped ? this.getGroups() : undefined;
+ return groups ? this.aggregateGroups(groups, null, 'first') : this.items[0];
+ },
+ /**
+ * Returns the last item in the collection.
+ * @param {Boolean} [grouped] `true` to extract the first item in each group. Only applies if
+ * a {@link #grouper} is active in the collection.
+ * @return {Object} The last item in the collection. If the grouped parameter is passed,
+ * see {@link #aggregateByGroup} for information on the return type.
+ * @since 5.0.0
+ */
+ last: function(grouped) {
+ var groups = grouped ? this.getGroups() : undefined;
+ return groups ? this.aggregateGroups(groups, null, 'last') : this.items[this.length - 1];
+ },
+ /**
+ * Returns the item associated with the passed key.
+ * @param {String/Number} key The key of the item.
+ * @return {Object} The item associated with the passed key.
+ * @since 5.0.0
+ */
+ get: function(key) {
+ return this.map[key];
+ },
+ /**
+ * Returns the item at the specified index.
+ * @param {Number} index The index of the item.
+ * @return {Object} The item at the specified index.
+ * @since 5.0.0
+ */
+ getAt: function(index) {
+ return this.items[index];
+ },
+ /**
+ * Returns the item associated with the passed key.
+ * @param {String/Number} key The key of the item.
+ * @return {Object} The item associated with the passed key.
+ * @since 5.0.0
+ */
+ getByKey: function(key) {
+ return this.map[key];
+ },
+ /**
+ * Returns the number of items in the collection.
+ * @return {Number} the number of items in the collection.
+ * @since 5.0.0
+ */
+ getCount: function() {
+ return this.length;
+ },
+ /**
+ * A function which will be called, passing an object belonging to this collection.
+ * The function should return the key by which that object will be indexed. This key
+ * must be unique to this item as only one item with this key will be retained.
+ *
+ * The default implementation looks basically like this (give or take special case
+ * handling of 0):
+ *
+ * function getKey (item) {
+ * return item.id || item._id || item.getId();
+ * }
+ *
+ * You can provide your own implementation by passing the `keyFn` config.
+ *
+ * For example, to hold items that have a unique "name" property:
+ *
+ * var elementCollection = new Ext.util.Collection({
+ * keyFn: function (item) {
+ * return item.name;
+ * }
+ * });
+ *
+ * The collection can have `extraKeys` if items need to be quickly looked up by other
+ * (potentially non-unique) properties.
+ *
+ * @param {Object} item The item.
+ * @return {Object} The key for the passed item.
+ * @since 5.0.0
+ */
+ getKey: function(item) {
+ var id = item.id;
+ return (id === 0 || id) ? id : ((id = item._id) === 0 || id) ? id : item.getId();
+ },
+ /**
+ * Returns a range of items in this collection
+ * @param {Number} [begin=0] The index of the first item to get.
+ * @param {Number} [end] The ending index. The item at this index is *not* included.
+ * @return {Array} An array of items
+ * @since 5.0.0
+ */
+ getRange: function(begin, end) {
+ var items = this.items,
+ length = items.length,
+ range;
+ if (begin > end) {
+ Ext.raise('Inverted range passed to Collection.getRange: [' + begin + ',' + end + ']');
+ }
+ if (!length) {
+ range = [];
+ } else {
+ range = Ext.Number.clipIndices(length, [
+ begin,
+ end
+ ]);
+ range = items.slice(range[0], range[1]);
+ }
+ return range;
+ },
+ /**
+ * @method getSource
+ * Returns all unfiltered items in the Collection when the Collection has been
+ * filtered. Returns `null` when the Collection is not filtered.
+ * @return {Ext.util.Collection} items All unfiltered items (or `null` when the
+ * Collection is not filtered)
+ */
+ /**
+ * Returns an array of values for the specified (sub) property.
+ *
+ * For example, to get an array of "name" properties from a collection of records (of
+ * `Ext.data.Model` objects):
+ *
+ * var names = collection.getValues('name', 'data');
+ *
+ * @param {String} property The property to collect on
+ * @param {String} [root] 'root' property to extract the first argument from. This is
+ * used mainly when operating on fields in records, where the fields are all stored
+ * inside the 'data' object.
+ * @param {Number} [start=0] The index of the first item to include.
+ * @param {Number} [end] The index at which to stop getting values. The value of this
+ * item is *not* included.
+ * @return {Object[]} The array of values.
+ * @since 5.0.0
+ */
+ getValues: function(property, root, start, end) {
+ var items = this.items,
+ range = Ext.Number.clipIndices(items.length, [
+ start,
+ end
+ ]),
+ ret = [],
+ i, value;
+ for (i = range[0] , end = range[1]; i < end; ++i) {
+ value = items[i];
+ value = (root ? value[root] : value)[property];
+ ret.push(value);
+ }
+ return ret;
+ },
+ /**
+ * Returns index within the collection of the passed Object.
+ * @param {Object} item The item to find.
+ * @return {Number} The index of the item or -1 if not found.
+ * @since 5.0.0
+ */
+ indexOf: function(item) {
+ var key;
+ if (!item) {
+ return -1;
+ }
+ key = this.getKey(item);
+ return this.indexOfKey(key);
+ },
+ /**
+ * Returns index within the collection of the passed key.
+ * @param {Object} key The key to find.
+ * @return {Number} The index of the item or -1 if not found.
+ * @since 5.0.0
+ */
+ indexOfKey: function(key) {
+ var me = this,
+ indices = me.indices;
+ if (key in me.map) {
+ if (!indices) {
+ indices = me.getIndices();
+ }
+ return indices[key];
+ }
+ return -1;
+ },
+ /**
+ * Inserts one or more items to the collection. The `index` value is the position at
+ * which the first item will be placed. The items starting at that position will be
+ * shifted to make room.
+ *
+ * @param {Number} index The index at which to insert the item(s).
+ * @param {Object/Object[]} item The item or items to add.
+ * @return {Object/Object[]} The item or items added.
+ * @since 5.0.0
+ */
+ insert: function(index, item) {
+ var me = this,
+ items = me.decodeItems(arguments, 1),
+ ret = items;
+ if (items.length) {
+ me.splice(index, 0, items);
+ ret = (items.length === 1) ? items[0] : items;
+ }
+ return ret;
+ },
+ /**
+ * This method should be called when an item in this collection has been modified. If
+ * the collection is sorted or filtered the result of modifying an item needs to be
+ * reflected in the collection. If the item's key is also being modified, it is best
+ * to pass the `oldKey` to this same call rather than call `updateKey` separately.
+ *
+ * @param {Object} item The item that was modified.
+ * @param {String[]} [modified] The names of the modified properties of the item.
+ * @param {String/Number} [oldKey] Passed if the item's key was also modified.
+ * @param {Object} meta (private)
+ * @since 5.0.0
+ */
+ itemChanged: function(item, modified, oldKey, meta) {
+ var me = this,
+ keyChanged = oldKey !== undefined,
+ filtered = me.filtered && me.getAutoFilter(),
+ filterChanged = false,
+ itemMovement = 0,
+ items = me.items,
+ last = me.length - 1,
+ // one or zero items is not really sorted
+ // CAN be called on an empty Collection
+ // A TreeStore can call afterEdit on a hidden root before
+ // any child nodes exist in the store.
+ sorted = me.sorted && last > 0 && me.getAutoSort(),
+ source = me.getSource(),
+ toRemove = 0,
+ itemFiltered = false,
+ wasFiltered = false,
+ details, newKey, sortFn, toAdd, index, newIndex;
+ // We are owned, we cannot react, inform owning collection.
+ if (source && !source.updating) {
+ me.sourceUpdating = true;
+ source.itemChanged(item, modified, oldKey, meta);
+ me.sourceUpdating = false;
+ } else // Root Collection has been informed.
+ // Change is propagating downward from root.
+ {
+ newKey = me.getKey(item);
+ if (filtered) {
+ index = me.indexOfKey(keyChanged ? oldKey : newKey);
+ wasFiltered = (index < 0);
+ itemFiltered = me.isItemFiltered(item);
+ filterChanged = (wasFiltered !== itemFiltered);
+ }
+ if (filterChanged) {
+ if (itemFiltered) {
+ toRemove = [
+ item
+ ];
+ newIndex = -1;
+ } else {
+ toAdd = [
+ item
+ ];
+ newIndex = me.length;
+ }
+ }
+ // this will be ignored if sorted
+ // If sorted, the newIndex must be reported correctly in the beforeitemchange
+ // and itemchange events.
+ // Even though splice ignores the parameter and calculates the insertion point
+ else if (sorted && !itemFiltered) {
+ // If we are sorted and there are 2 or more items make sure this item is at
+ // the proper index.
+ if (!filtered) {
+ // If the filter has not changed we may need to move the item but if
+ // there is a filter we have already determined its index.
+ index = me.indexOfKey(keyChanged ? oldKey : newKey);
+ }
+ sortFn = me.getSortFn();
+ if (index !== -1) {
+ if (index && sortFn(items[index - 1], items[index]) > 0) {
+ // If this item is not the first and the item before it compares as
+ // greater-than then item needs to move left since it is less-than
+ // item[index - 1].
+ itemMovement = -1;
+ // We have to bound the binarySearch or else the presence of the
+ // out-of-order "item" would break it.
+ newIndex = Ext.Array.binarySearch(items, item, 0, index, sortFn);
+ } else if (index < last && sortFn(items[index], items[index + 1]) > 0) {
+ // If this item is not the last and the item after it compares as
+ // less-than then item needs to move right since it is greater-than
+ // item[index + 1].
+ itemMovement = 1;
+ // We have to bound the binarySearch or else the presence of the
+ // out-of-order "item" would break it.
+ newIndex = Ext.Array.binarySearch(items, item, index + 1, sortFn);
+ }
+ if (itemMovement) {
+ toAdd = [
+ item
+ ];
+ }
+ }
+ }
+ // One may be tempted to avoid this notification when none of our three vars
+ // are true, *but* the problem with that is that these three changes we care
+ // about are only what this collection cares about. Child collections or
+ // outside parties still need to know that the item has changed in some way.
+ // We do NOT adjust the newIndex reported here to allow for position *after*
+ // the item has been removed
+ // We report the "visual" position at which the item would be inserted as if
+ // it were new.
+ details = {
+ item: item,
+ key: newKey,
+ index: newIndex,
+ filterChanged: filterChanged,
+ keyChanged: keyChanged,
+ indexChanged: !!itemMovement,
+ filtered: itemFiltered,
+ oldIndex: index,
+ newIndex: newIndex,
+ wasFiltered: wasFiltered,
+ meta: meta
+ };
+ if (keyChanged) {
+ details.oldKey = oldKey;
+ }
+ if (modified) {
+ details.modified = modified;
+ }
+ ++me.generation;
+ me.beginUpdate();
+ me.notify('beforeitemchange', [
+ details
+ ]);
+ if (keyChanged) {
+ me.updateKey(item, oldKey, details);
+ }
+ if (toAdd || toRemove) {
+ // In sorted mode (which is the only time we get here), newIndex is
+ // correct but *ignored* by splice since it has to assume that *insert*
+ // index values need to be determined internally. In other words, the
+ // first argument here is both the remove and insert index but in sorted
+ // mode the insert index is calculated by splice.
+ me.splice(newIndex, toRemove, toAdd);
+ }
+ // Ensure that the newIndex always refers to the item the insertion is *before*.
+ // Ensure that the oldIndex always refers to the item the insertion was *before*.
+ //
+ // Before change to "c" to "h": | Before change "i" to "d":
+ // |
+ // +---+---+---+---+---+---+ | +---+---+---+---+---+---+
+ // | a | c | e | g | i | k | | | a | c | e | g | i | k |
+ // +---+---+---+---+---+---+ | +---+---+---+---+---+---+
+ // 0 1 2 3 4 5 | 0 1 2 3 4 5
+ // ^ ^ | ^ ^
+ // | | | | |
+ // oldIndex newIndex | newIndex oldIndex
+ // |
+ // After change to "c" to "h": | After change "i" to "d":
+ // |
+ // +---+---+---+---+---+---+ | +---+---+---+---+---+---+
+ // | a | e | g | h | i | k | | | a | c | d | e | g | k |
+ // +---+---+---+---+---+---+ | +---+---+---+---+---+---+
+ // 0 1 2 3 4 5 | 0 1 2 3 4 5
+ // ^ ^ | ^ ^
+ // | | | | |
+ // oldIndex newIndex | newIndex oldIndex
+ //
+ if (itemMovement > 0) {
+ details.newIndex--;
+ } else if (itemMovement < 0) {
+ details.oldIndex++;
+ }
+ // Divergence depending on whether the record if filtered out at this level
+ // in a chaining hierarchy. Child collections of this collection will not care
+ // about filtereditemchange because the record is not in them.
+ // Stores however will still need to know because the record *is* in them,
+ // just filtered.
+ me.notify(itemFiltered ? 'filtereditemchange' : 'itemchange', [
+ details
+ ]);
+ me.endUpdate();
+ }
+ },
+ /**
+ * Remove an item from the collection.
+ * @param {Object/Object[]} item The item or items to remove.
+ * @return {Number} The number of items removed.
+ * @since 5.0.0
+ */
+ remove: function(item) {
+ var me = this,
+ items = me.decodeRemoveItems(arguments, 0),
+ length = me.length;
+ me.splice(0, items);
+ return length - me.length;
+ },
+ /**
+ * Remove all items in the collection.
+ * @return {Ext.util.Collection} This object.
+ * @since 5.0.0
+ */
+ removeAll: function() {
+ var me = this,
+ length = me.length;
+ if (me.generation && length) {
+ me.splice(0, length);
+ }
+ return me;
+ },
+ /**
+ * Remove an item from a specified index in the collection.
+ * @param {Number} index The index within the collection of the item to remove.
+ * @param {Number} [count=1] The number of items to remove.
+ * @return {Object/Number} If `count` was 1 and the item was removed, that item is
+ * returned. Otherwise the number of items removed is returned.
+ * @since 5.0.0
+ */
+ removeAt: function(index, count) {
+ var me = this,
+ length = me.length,
+ Num = Ext.Number,
+ range = Num.clipIndices(length, [
+ index,
+ (count === undefined) ? 1 : count
+ ], Num.Clip.COUNT),
+ n = range[0],
+ removeCount = range[1] - n,
+ item = (removeCount === 1) && me.getAt(n),
+ removed;
+ me.splice(n, removeCount);
+ removed = me.length - length;
+ return (item && removed) ? item : removed;
+ },
+ /**
+ * Removes the item associated with the passed key from the collection.
+ * @param {String} key The key of the item to remove.
+ * @return {Object} Only returned if removing at a specified key. The item removed or
+ * `false` if no item was removed.
+ * @since 5.0.0
+ */
+ removeByKey: function(key) {
+ var item = this.getByKey(key);
+ if (!item || !this.remove(item)) {
+ return false;
+ }
+ return item;
+ },
+ /**
+ * @private
+ * Replace an old entry with a new entry of the same key if the entry existed.
+ * @param {Object} item The item to insert.
+ * @return {Object} inserted The item inserted.
+ */
+ replace: function(item) {
+ var index = this.indexOf(item);
+ if (index === -1) {
+ this.add(item);
+ } else {
+ this.insert(index, item);
+ }
+ },
+ /**
+ * This method is basically the same as the JavaScript Array splice method.
+ *
+ * Negative indexes are interpreted starting at the end of the collection. That is,
+ * a value of -1 indicates the last item, or equivalent to `length - 1`.
+ *
+ * @param {Number} index The index at which to add or remove items.
+ * @param {Number/Object[]} toRemove The number of items to remove or an array of the
+ * items to remove.
+ * @param {Object[]} [toAdd] The items to insert at the given `index`.
+ * @since 5.0.0
+ */
+ splice: function(index, toRemove, toAdd) {
+ var me = this,
+ autoSort = me.sorted && me.getAutoSort(),
+ map = me.map,
+ items = me.items,
+ length = me.length,
+ removeItems = (toRemove instanceof Array) ? me.decodeRemoveItems(toRemove) : null,
+ isRemoveCount = !removeItems,
+ Num = Ext.Number,
+ range = Num.clipIndices(length, [
+ index,
+ isRemoveCount ? toRemove : 0
+ ], Num.Clip.COUNT),
+ begin = range[0],
+ end = range[1],
+ // Determine how many items we might actually remove:
+ removeCount = end - begin,
+ newItems = me.decodeItems(arguments, 2),
+ newCount = newItems ? newItems.length : 0,
+ addItems, newItemsMap, removeMap,
+ insertAt = begin,
+ indices = me.indices || ((newCount || removeItems) ? me.getIndices() : null),
+ adds = null,
+ removes = removeCount ? [
+ begin
+ ] : null,
+ newKeys = null,
+ source = me.getSource(),
+ chunk, chunkItems, chunks, i, item, itemIndex, k, key, keys, n, duplicates, sorters;
+ if (source && !source.updating) {
+ // Modifying the content of a child collection has to be translated into a
+ // change of its source. Because the source has all of the items of the child
+ // (but likely at different indices) we can translate "index" and convert a
+ // "removeCount" request into a "removeItems" request.
+ if (isRemoveCount) {
+ removeItems = [];
+ for (i = 0; i < removeCount; ++i) {
+ removeItems.push(items[begin + i]);
+ }
+ }
+ if (begin < length) {
+ // Map index based on the item at that index since that item will be in
+ // the source collection.
+ i = source.indexOf(items[begin]);
+ } else {
+ // Map end of this collection to end of the source collection.
+ i = source.length;
+ }
+ // When we react to the source add in onCollectionAdd, we must honour this
+ // requested index.
+ me.requestedIndex = index;
+ source.splice(i, removeItems, newItems);
+ delete me.requestedIndex;
+ return me;
+ }
+ // Loop over the newItems because they could already be in the collection or may
+ // be replacing items in the collection that just happen to have the same key. In
+ // this case, those items must be removed as well. Since we need to call getKey
+ // on each newItem to do this we may as well keep those keys for later.
+ if (newCount) {
+ addItems = newItems;
+ newKeys = [];
+ newItemsMap = {};
+ // If this collection is sorted we will eventually need to sort addItems so
+ // do that now so we can line up the newKeys properly. We optimize for the
+ // case where we have no duplicates. It would be more expensive to do this
+ // in two passes in an attempt to take advantage of removed duplicates.
+ if (autoSort) {
+ // We'll need the sorters later as well
+ sorters = me.getSorters();
+ if (newCount > 1) {
+ if (!addItems.$cloned) {
+ newItems = addItems = addItems.slice(0);
+ }
+ me.sortData(addItems);
+ }
+ }
+ for (i = 0; i < newCount; ++i) {
+ key = me.getKey(item = newItems[i]);
+ if ((k = newItemsMap[key]) !== undefined) {
+ // Duplicates in the incoming newItems need to be discarded keeping the
+ // last of the duplicates. We add the index of the last duplicate of
+ // this key to the "duplicates" map.
+ (duplicates || (duplicates = {}))[k] = 1;
+ } else {
+ // This item's index is outside the remove range, so we need to remove
+ // some extra stuff. Only the first occurrence of a given key in the
+ // newItems needs this processing.
+ itemIndex = indices[key];
+ if (itemIndex < begin || end <= itemIndex) {
+ (removes || (removes = [])).push(itemIndex);
+ }
+ }
+ // might be the first
+ newItemsMap[key] = i;
+ // track the last index of this key in newItems
+ newKeys.push(key);
+ }
+ // must correspond 1-to-1 with newItems
+ if (duplicates) {
+ keys = newKeys;
+ addItems = [];
+ newKeys = [];
+ addItems.$cloned = true;
+ for (i = 0; i < newCount; ++i) {
+ if (!duplicates[i]) {
+ item = newItems[i];
+ addItems.push(item);
+ newKeys.push(keys[i]);
+ }
+ }
+ newCount = addItems.length;
+ }
+ adds = {
+ // at: insertAt, // must fill this in later
+ // next: null, // only set by spliceMerge
+ // replaced: null, // must fill this in later
+ items: addItems,
+ keys: newKeys
+ };
+ }
+ // If we are given a set of items to remove, map them to their indices.
+ for (i = removeItems ? removeItems.length : 0; i-- > 0; ) {
+ key = me.getKey(removeItems[i]);
+ if ((itemIndex = indices[key]) !== undefined) {
+ // ignore items we don't have (probably due to filtering)
+ (removes || (removes = [])).push(itemIndex);
+ }
+ }
+ // might be the first remove
+ if (!adds && !removes) {
+ return me;
+ }
+ me.beginUpdate();
+ // Now we that everything we need to remove has its index in the removes array.
+ // We start by sorting the array so we can coalesce the index values into chunks
+ // or ranges.
+ if (removes) {
+ chunk = null;
+ chunks = [];
+ removeMap = {};
+ if (removes.length > 1) {
+ removes.sort(Ext.Array.numericSortFn);
+ }
+ // Coalesce the index array into chunks of (index, count) pairs for efficient
+ // removal.
+ for (i = 0 , n = removes.length; i < n; ++i) {
+ key = me.getKey(item = items[itemIndex = removes[i]]);
+ if (!(key in map)) {
+
+ continue;
+ }
+ // Avoids 2nd loop of removed items but also means we won't process any
+ // given item twice (in case of duplicates in removeItems).
+ delete map[key];
+ // Consider chunk = { at: 1, items: [ item1, item2 ] }
+ //
+ // +---+---+---+---+---+---+
+ // | | x | x | | | |
+ // +---+---+---+---+---+---+
+ // 0 1 2 3 4 5
+ //
+ // If we are adding an itemIndex > 3 we need a new chunk.
+ //
+ if (!chunk || itemIndex > (chunk.at + chunkItems.length)) {
+ chunks.push(chunk = {
+ at: itemIndex,
+ items: (chunkItems = []),
+ keys: (keys = []),
+ map: removeMap,
+ next: chunk,
+ replacement: adds
+ });
+ // Point "replaced" at the last chunk
+ if (adds) {
+ adds.replaced = chunk;
+ }
+ }
+ chunkItems.push(removeMap[key] = item);
+ keys.push(key);
+ // NOTE: For a long time the test below was "itemIndex < insertAt - 1",
+ // but that does not work if the itemIndex is at the very end. This would
+ // produce the "at" and "atItem" referencing the item being inserted
+ // rather than the item to which the insert was relative.
+ if (itemIndex < insertAt) {
+ // If the removal is ahead of the insertion point specified, we need
+ // to move the insertAt backwards.
+ //
+ // Consider the following splice:
+ //
+ // collection.splice(3, 2, [ { id: 'b' } ]);
+ //
+ // +---+---+---+---+---+---+
+ // | a | b | c | x | y | d |
+ // +---+---+---+---+---+---+
+ // 0 1 2 3 4 5
+ // ^ ^ ^
+ // | \ /
+ // replace remove
+ //
+ // The intent is to replace x and y with the new item at index=3. But
+ // since the new item has the same key as the item at index=1, that
+ // item must be replaced. The resulting collection will be:
+ //
+ // +---+---+---+---+
+ // | a | c | b | d |
+ // +---+---+---+---+
+ // 0 1 2 3
+ //
+ --insertAt;
+ }
+ if (removeCount > 1 && itemIndex === begin) {
+ // To account for the given range to remove we started by putting the
+ // index of the first such item ("begin") in the array. When we find
+ // it in this loop we have to process all of the items and add them
+ // to the current chunk. The following trick allows us to repeat the
+ // loop for each item in the removeCount.
+ //
+ --removeCount;
+ // countdown...
+ removes[i--] = ++begin;
+ }
+ }
+ // backup and increment begin
+ // for (removes)
+ if (adds) {
+ adds.at = insertAt;
+ }
+ // we have the correct(ed) insertAt now
+ // Loop over the chunks in reverse so as to not invalidate index values on
+ // earlier chunks.
+ for (k = chunks.length; k-- > 0; ) {
+ chunk = chunks[k];
+ i = chunk.at;
+ n = chunk.items.length;
+ if (i + n < length) {
+ // If we are removing the tail of the collection, we can keep the
+ // indices for the rest of the things... otherwise we need to zap it
+ // and fix up later.
+ me.indices = indices = null;
+ }
+ me.length = length -= n;
+ // We can use splice directly. The IE8 bug which Ext.Array works around
+ // only affects *insertion*
+ // http://social.msdn.microsoft.com/Forums/en-US/iewebdevelopment/thread/6e946d03-e09f-4b22-a4dd-cd5e276bf05a/
+ // Ext.Array.erase(items, i, n);
+ items.splice(i, n);
+ if (indices) {
+ keys = chunk.keys;
+ for (i = 0; i < n; ++i) {
+ delete indices[keys[i]];
+ }
+ }
+ ++me.generation;
+ me.notify('remove', [
+ chunk
+ ]);
+ }
+ }
+ // if (removes)
+ if (adds) {
+ if (autoSort && newCount > 1 && length) {
+ me.spliceMerge(addItems, newKeys);
+ } else {
+ if (autoSort) {
+ if (newCount > 1) {
+ // We have multiple addItems but we are empty, so just add at 0
+ insertAt = 0;
+ me.indices = indices = null;
+ } else {
+ // If we are adding one item we can position it properly now and
+ // avoid a full sort.
+ insertAt = sorters.findInsertionIndex(adds.items[0], items, me.getSortFn(), index);
+ }
+ }
+ if (insertAt === length) {
+ end = insertAt;
+ // Inser items backwards. This way, when the first item is pushed the
+ // array is sized to as large as we're going to need it to be.
+ for (i = addItems.length - 1; i >= 0; --i) {
+ items[end + i] = addItems[i];
+ }
+ // The indices may have been regenerated, so we need to check if they have been
+ // and update them
+ indices = me.indices;
+ if (indices) {
+ for (i = 0; i < newCount; ++i) {
+ indices[newKeys[i]] = insertAt + i;
+ }
+ }
+ } else {
+ // inserting
+ me.indices = null;
+ Ext.Array.insert(items, insertAt, addItems);
+ }
+ for (i = 0; i < newCount; ++i) {
+ map[newKeys[i]] = addItems[i];
+ }
+ me.length += newCount;
+ adds.at = insertAt;
+ adds.atItem = insertAt === 0 ? null : items[insertAt - 1];
+ ++me.generation;
+ me.notify('add', [
+ adds
+ ]);
+ }
+ }
+ // if (adds)
+ me.endUpdate();
+ return me;
+ },
+ /**
+ * This method calls the supplied function `fn` between `beginUpdate` and `endUpdate`
+ * calls.
+ *
+ * collection.update(function () {
+ * // Perform multiple collection updates...
+ *
+ * collection.add(item);
+ * // ...
+ *
+ * collection.insert(index, otherItem);
+ * //...
+ *
+ * collection.remove(someItem);
+ * });
+ *
+ * @param {Function} fn The function to call that will modify this collection.
+ * @param {Ext.util.Collection} fn.collection This collection.
+ * @param {Object} [scope=this] The `this` pointer to use when calling `fn`.
+ * @return {Object} Returns the value returned from `fn` (typically `undefined`).
+ * @since 5.0.0
+ */
+ update: function(fn, scope) {
+ var me = this;
+ me.beginUpdate();
+ try {
+ return fn.call(scope || me, me);
+ } catch (e) {
+ Ext.log.error(this.$className + ': Unhandled Exception: ', e.description || e.message);
+ throw e;
+ } finally {
+ me.endUpdate();
+ }
+ },
+ /**
+ * Change the key for an existing item in the collection. If the old key does not
+ * exist this call does nothing. Even so, it is highly recommended to *avoid* calling
+ * this method for an `item` that is not a member of this collection.
+ *
+ * @param {Object} item The item whose key has changed. The `item` should be a member
+ * of this collection.
+ * @param {String} oldKey The old key for the `item`.
+ * @param details
+ * @since 5.0.0
+ */
+ updateKey: function(item, oldKey, details) {
+ var me = this,
+ map = me.map,
+ indices = me.indices,
+ source = me.getSource(),
+ newKey;
+ if (source && !source.updating) {
+ // If we are being told of the key change and the source has the same idea
+ // on keying the item, push the change down instead.
+ source.updateKey(item, oldKey);
+ } else if ((newKey = me.getKey(item)) !== oldKey) {
+ // If the key has changed and "item" is the item mapped to the oldKey and
+ // there is no collision with an item with the newKey, we can proceed.
+ if (map[oldKey] === item && !(newKey in map)) {
+ delete map[oldKey];
+ // We need to mark ourselves as updating so that observing collections
+ // don't reflect the updateKey back to us (see above check) but this is
+ // not really a normal update cycle so we don't call begin/endUpdate.
+ me.updating++;
+ me.generation++;
+ map[newKey] = item;
+ if (indices) {
+ indices[newKey] = indices[oldKey];
+ delete indices[oldKey];
+ }
+ me.notify('updatekey', [
+ Ext.apply({
+ item: item,
+ newKey: newKey,
+ oldKey: oldKey
+ }, details)
+ ]);
+ me.updating--;
+ } else {
+ // It may be that the item is (somehow) already in the map using the
+ // newKey or that there is no item in the map with the oldKey. These
+ // are not errors.
+ if (newKey in map && map[newKey] !== item) {
+ // There is a different item in the map with the newKey which is an
+ // error. To properly handle this, add the item instead.
+ Ext.raise('Duplicate newKey "' + newKey + '" for item with oldKey "' + oldKey + '"');
+ }
+ if (oldKey in map && map[oldKey] !== item) {
+ // There is a different item in the map with the oldKey which is also
+ // an error. Do not call this method for items that are not part of
+ // the collection.
+ Ext.raise('Incorrect oldKey "' + oldKey + '" for item with newKey "' + newKey + '"');
+ }
+ }
+ }
+ },
+ findInsertIndex: function(item, defaultIndex) {
+ var source = this.getSource(),
+ sourceItems = source.items,
+ i = source.indexOf(item) - 1,
+ sourceItem, index;
+ while (i > -1) {
+ sourceItem = sourceItems[i];
+ index = this.indexOf(sourceItem);
+ if (index > -1) {
+ return index + 1;
+ }
+ --i;
+ }
+ // If we get here we didn't find any item in the parent before us...
+ return defaultIndex;
+ },
+ //-------------------------------------------------------------------------
+ // Calls from the source Collection:
+ /**
+ * This method is called when items are added to the `source` collection. This is
+ * equivalent to the `{@link #event-add add}` event but is called before the `add`
+ * event is fired.
+ * @param {Ext.util.Collection} source The source collection.
+ * @param {Object} details The `details` of the `{@link #event-add add}` event.
+ * @private
+ * @since 5.0.0
+ */
+ onCollectionAdd: function(source, details) {
+ var me = this,
+ atItem = details.atItem,
+ items = me.transformItems(details.items),
+ requestedIndex = me.requestedIndex,
+ filtered, index, copy, i, item, n;
+ // If we have a requestedIndex, it means the add/insert was on our collection,
+ // so try use that specified index to do the insertion.
+ if (requestedIndex !== undefined) {
+ index = requestedIndex;
+ } else {
+ // If !atItem, the insert was at 0 in the source, so use [1] as beforeItem:
+ item = atItem || source.items[1];
+ index = item ? me.indexOf(item) : -1;
+ if (index > -1) {
+ // We also have that item in our collection, we need to insert after it
+ // if atItem was passed (since that is the item after which the new item
+ // was inserted in the source).
+ if (atItem) {
+ ++index;
+ }
+ }
+ // We can't find the reference item in our collection, which means it's probably
+ // filtered out, so we need to search for an appropriate index. Pass the first
+ // item and work back to find at good reference. Failing that, insert at front
+ // or back based on front/back in source.
+ else if (!me.sorted) {
+ index = me.findInsertIndex(items[0], details.at ? me.length : 0);
+ }
+ }
+ if (me.getAutoFilter() && me.filtered) {
+ for (i = 0 , n = items.length; i < n; ++i) {
+ item = items[i];
+ if (me.isItemFiltered(item)) {
+ // If we have an item that is filtered out of this collection, we need
+ // to make a copy of the items up to this point.
+ if (!copy) {
+ copy = items.slice(0, i);
+ }
+ if (!filtered) {
+ filtered = [];
+ }
+ filtered.push(item);
+ } else if (copy) {
+ // If we have a copy of the items, we need to put this item in that
+ // copy since it is not being filtered out.
+ copy.push(item);
+ }
+ }
+ }
+ me.splice((index < 0) ? me.length : index, 0, copy || items);
+ if (filtered) {
+ // Private for now. We may want to let any observers know we just
+ // added these items but got filtered out
+ me.notify('filteradd', [
+ filtered
+ ]);
+ }
+ },
+ /**
+ * This method is called when an item is modified in the `source` collection. This is
+ * equivalent to the `{@link #event-beforeitemchange beforeitemchange}` event but is
+ * called before the `beforeitemchange` event is fired.
+ * @param {Ext.util.Collection} source The source collection.
+ * @param {Object} details The `details` of the
+ * `{@link #event-beforeitemchange beforeitemchange}` event.
+ * @private
+ * @since 5.0.0
+ */
+ onCollectionBeforeItemChange: function(source, details) {
+ var me = this;
+ // Drop the next few confusing events:
+ me.onCollectionUpdateKey = null;
+ // If this flag is true it means we're inside itemchanged, so this will be fired
+ // shortly, don't fire it twice
+ if (!me.sourceUpdating) {
+ me.notify('beforeitemchange', [
+ details
+ ]);
+ }
+ },
+ /**
+ * This method is called when the `source` collection starts updating. This is
+ * equivalent to the `{@link #event-beginupdate beginupdate}` event but is called
+ * before the `beginupdate` event is fired.
+ * @param {Ext.util.Collection} source The source collection.
+ * @private
+ * @since 5.0.0
+ */
+ onCollectionBeginUpdate: function() {
+ this.beginUpdate();
+ },
+ /**
+ * This method is called when the `source` collection finishes updating. This is
+ * equivalent to the `{@link #event-endupdate endupdate}` event but is called before
+ * the `endupdate` event is fired.
+ * @param {Ext.util.Collection} source The source collection.
+ * @private
+ * @since 5.0.0
+ */
+ onCollectionEndUpdate: function() {
+ this.endUpdate();
+ },
+ /**
+ * This method is called when an item is modified in the `source` collection. This is
+ * equivalent to the `{@link #event-itemchange itemchange}` event but is called before
+ * the `itemchange` event is fired.
+ * @param {Ext.util.Collection} source The source collection.
+ * @param {Object} details The `details` of the `{@link #event-itemchange itemchange}`
+ * event.
+ * @private
+ * @since 5.0.0
+ */
+ onCollectionItemChange: function(source, details) {
+ // Restore things:
+ delete this.onCollectionUpdateKey;
+ this.itemChanged(details.item, details.modified, details.oldKey, details.meta);
+ },
+ onCollectionFilteredItemChange: function() {
+ // Restore things:
+ delete this.onCollectionUpdateKey;
+ },
+ /**
+ * This method is called when the `source` collection refreshes. This is equivalent to
+ * the `{@link #event-refresh refresh}` event but is called before the `refresh` event
+ * is fired.
+ * @param {Ext.util.Collection} source The source collection.
+ * @private
+ * @since 5.0.0
+ */
+ onCollectionRefresh: function(source) {
+ var me = this,
+ map = {},
+ indices = {},
+ items = me.items,
+ sourceItems = me.transformItems(source.items),
+ filterFn = me.getFilterFn(),
+ i, item, key, length, newLength;
+ // Perform a non-destructive filter of the source's items array into the
+ // *existing* items array because stores give away references to this
+ // collection's items array.
+ if (me.filtered && me.getAutoFilter()) {
+ for (i = 0 , newLength = 0 , length = sourceItems.length; i < length; i++) {
+ if (filterFn(sourceItems[i])) {
+ items[newLength++] = sourceItems[i];
+ }
+ }
+ items.length = newLength;
+ } else {
+ items.length = 0;
+ items.push.apply(items, sourceItems);
+ }
+ if (me.sorted) {
+ me.sortData(items);
+ }
+ me.length = length = items.length;
+ me.map = map;
+ me.indices = indices;
+ for (i = 0; i < length; ++i) {
+ key = me.getKey(item = items[i]);
+ map[key] = item;
+ indices[key] = i;
+ }
+ ++me.generation;
+ me.notify('refresh');
+ },
+ /**
+ * This method is called when items are removed from the `source` collection. This is
+ * equivalent to the `{@link #event-remove remove}` event but is called before the
+ * `remove` event is fired.
+ * @param {Ext.util.Collection} source The source collection.
+ * @param {Object} details The `details` of the `remove` event.
+ * @private
+ * @since 5.0.0
+ */
+ onCollectionRemove: function(source, details) {
+ this.splice(0, details.items);
+ },
+ /**
+ * @method onCollectionSort
+ * This method is called when the `source` collection is sorted. This is equivalent to
+ * the `{@link #event-sort sort}` event but is called before the `sort` event is fired.
+ * @param {Ext.util.Collection} source The source collection.
+ * @private
+ * @since 5.0.0
+ */
+ // onCollectionSort: function (source) {
+ // we ignore sorting of the source collection because we prefer our own order.
+ // },
+ /**
+ * This method is called when key changes in the `source` collection. This is
+ * equivalent to the `updatekey` event but is called before the `updatekey` event is
+ * fired.
+ * @param {Ext.util.Collection} source The source collection.
+ * @param {Object} details The `details` of the `updatekey` event.
+ * @private
+ * @since 5.0.0
+ */
+ onCollectionUpdateKey: function(source, details) {
+ this.updateKey(details.item, details.oldKey, details);
+ },
+ //-------------------------------------------------------------------------
+ // Private
+ /**
+ * @method average
+ * Averages property values from some or all of the items in this collection.
+ *
+ * @param {String} property The name of the property to average from each item.
+ * @param {Number} [begin] The index of the first item to include in the average.
+ * @param {Number} [end] The index at which to stop averaging `items`. The item at
+ * this index will *not* be included in the average.
+ * @return {Object} The result of averaging the specified property from the indicated
+ * items.
+ * @since 5.0.0
+ */
+ /**
+ * @method averageByGroup
+ * See {@link #average}. The result is partitioned by group.
+ *
+ * @param {String} property The name of the property to average from each item.
+ * @return {Object} The result of {@link #average}, partitioned by group. See
+ * {@link #aggregateByGroup}.
+ * @since 5.0.0
+ */
+ /**
+ * @method bounds
+ * Determines the minimum and maximum values for the specified property over some or
+ * all of the items in this collection.
+ *
+ * @param {String} property The name of the property from each item.
+ * @param {Number} [begin] The index of the first item to include in the bounds.
+ * @param {Number} [end] The index at which to stop in `items`. The item at this index
+ * will *not* be included in the bounds.
+ * @return {Array} An array `[min, max]` with the minimum and maximum of the specified
+ * property.
+ * @since 5.0.0
+ */
+ /**
+ * @method boundsByGroup
+ * See {@link #bounds}. The result is partitioned by group.
+ *
+ * @param {String} property The name of the property from each item.
+ * @return {Object} The result of {@link #bounds}, partitioned by group. See
+ * {@link #aggregateByGroup}.
+ * @since 5.0.0
+ */
+ /**
+ * @method count
+ * Determines the number of items in the collection.
+ *
+ * @return {Number} The number of items.
+ * @since 5.0.0
+ */
+ /**
+ * @method countByGroup
+ * See {@link #count}. The result is partitioned by group.
+ *
+ * @return {Object} The result of {@link #count}, partitioned by group. See
+ * {@link #aggregateByGroup}.
+ * @since 5.0.0
+ */
+ /**
+ * @method extremes
+ * Finds the items with the minimum and maximum for the specified property over some
+ * or all of the items in this collection.
+ *
+ * @param {String} property The name of the property from each item.
+ * @param {Number} [begin] The index of the first item to include.
+ * @param {Number} [end] The index at which to stop in `items`. The item at this index
+ * will *not* be included.
+ * @return {Array} An array `[minItem, maxItem]` with the items that have the minimum
+ * and maximum of the specified property.
+ * @since 5.0.0
+ */
+ /**
+ * @method extremesByGroup
+ * See {@link #extremes}. The result is partitioned by group.
+ *
+ * @param {String} property The name of the property from each item.
+ * @return {Object} The result of {@link #extremes}, partitioned by group. See
+ * {@link #aggregateByGroup}.
+ * @since 5.0.0
+ */
+ /**
+ * @method max
+ * Determines the maximum value for the specified property over some or all of the
+ * items in this collection.
+ *
+ * @param {String} property The name of the property from each item.
+ * @param {Number} [begin] The index of the first item to include in the maximum.
+ * @param {Number} [end] The index at which to stop in `items`. The item at this index
+ * will *not* be included in the maximum.
+ * @return {Object} The maximum of the specified property from the indicated items.
+ * @since 5.0.0
+ */
+ /**
+ * @method maxByGroup
+ * See {@link #max}. The result is partitioned by group.
+ *
+ * @param {String} property The name of the property from each item.
+ * @return {Object} The result of {@link #max}, partitioned by group. See
+ * {@link #aggregateByGroup}.
+ * @since 5.0.0
+ */
+ /**
+ * @method maxItem
+ * Finds the item with the maximum value for the specified property over some or all
+ * of the items in this collection.
+ *
+ * @param {String} property The name of the property from each item.
+ * @param {Number} [begin] The index of the first item to include in the maximum.
+ * @param {Number} [end] The index at which to stop in `items`. The item at this index
+ * will *not* be included in the maximum.
+ * @return {Object} The item with the maximum of the specified property from the
+ * indicated items.
+ * @since 5.0.0
+ */
+ /**
+ * @method maxItemByGroup
+ * See {@link #maxItem}. The result is partitioned by group.
+ *
+ * @param {String} property The name of the property from each item.
+ * @return {Object} The result of {@link #maxItem}, partitioned by group. See
+ * {@link #aggregateByGroup}.
+ * @since 5.0.0
+ */
+ /**
+ * @method min
+ * Determines the minimum value for the specified property over some or all of the
+ * items in this collection.
+ *
+ * @param {String} property The name of the property from each item.
+ * @param {Number} [begin] The index of the first item to include in the minimum.
+ * @param {Number} [end] The index at which to stop in `items`. The item at this index
+ * will *not* be included in the minimum.
+ * @return {Object} The minimum of the specified property from the indicated items.
+ * @since 5.0.0
+ */
+ /**
+ * @method minByGroup
+ * See {@link #min}. The result is partitioned by group.
+ *
+ * @param {String} property The name of the property from each item.
+ * @return {Object} The result of {@link #min}, partitioned by group. See
+ * {@link #aggregateByGroup}.
+ * @since 5.0.0
+ */
+ /**
+ * @method minItem
+ * Finds the item with the minimum value for the specified property over some or all
+ * of the items in this collection.
+ *
+ * @param {String} property The name of the property from each item.
+ * @param {Number} [begin] The index of the first item to include in the minimum.
+ * @param {Number} [end] The index at which to stop in `items`. The item at this index
+ * will *not* be included in the minimum.
+ * @return {Object} The item with the minimum of the specified property from the
+ * indicated items.
+ * @since 5.0.0
+ */
+ /**
+ * @method minItemByGroup
+ * See {@link #minItem}. The result is partitioned by group.
+ *
+ * @param {String} property The name of the property from each item.
+ * @return {Object} The result of {@link #minItem}, partitioned by group. See
+ * {@link #aggregateByGroup}.
+ * @since 5.0.0
+ */
+ /**
+ * @method sum
+ * Sums property values from some or all of the items in this collection.
+ *
+ * @param {String} property The name of the property to sum from each item.
+ * @param {Number} [begin] The index of the first item to include in the sum.
+ * @param {Number} [end] The index at which to stop summing `items`. The item at this
+ * index will *not* be included in the sum.
+ * @return {Object} The result of summing the specified property from the indicated
+ * items.
+ * @since 5.0.0
+ */
+ /**
+ * @method sumByGroup
+ * See {@link #sum}. The result is partitioned by group.
+ *
+ * @param {String} property The name of the property to sum from each item.
+ * @return {Object} The result of {@link #sum}, partitioned by group. See
+ * {@link #aggregateByGroup}.
+ * @since 5.0.0
+ */
+ _aggregators: {
+ average: function(items, begin, end, property, root) {
+ var n = end - begin;
+ return n && this._aggregators.sum.call(this, items, begin, end, property, root) / n;
+ },
+ bounds: function(items, begin, end, property, root) {
+ var value, max, min, i;
+ for (i = begin; i < end; ++i) {
+ value = items[i];
+ value = (root ? value[root] : value)[property];
+ // First pass max and min are undefined and since nothing is less than
+ // or greater than undefined we always evaluate these "if" statements as
+ // true to pick up the first value as both max and min.
+ if (!(value < max)) {
+ // jshint ignore:line
+ max = value;
+ }
+ if (!(value > min)) {
+ // jshint ignore:line
+ min = value;
+ }
+ }
+ return [
+ min,
+ max
+ ];
+ },
+ count: function(items) {
+ return items.length;
+ },
+ extremes: function(items, begin, end, property, root) {
+ var most = null,
+ least = null,
+ i, item, max, min, value;
+ for (i = begin; i < end; ++i) {
+ item = items[i];
+ value = (root ? item[root] : item)[property];
+ // Same trick as "bounds"
+ if (!(value < max)) {
+ // jshint ignore:line
+ max = value;
+ most = item;
+ }
+ if (!(value > min)) {
+ // jshint ignore:line
+ min = value;
+ least = item;
+ }
+ }
+ return [
+ least,
+ most
+ ];
+ },
+ max: function(items, begin, end, property, root) {
+ var b = this._aggregators.bounds.call(this, items, begin, end, property, root);
+ return b[1];
+ },
+ maxItem: function(items, begin, end, property, root) {
+ var b = this._aggregators.extremes.call(this, items, begin, end, property, root);
+ return b[1];
+ },
+ min: function(items, begin, end, property, root) {
+ var b = this._aggregators.bounds.call(this, items, begin, end, property, root);
+ return b[0];
+ },
+ minItem: function(items, begin, end, property, root) {
+ var b = this._aggregators.extremes.call(this, items, begin, end, property, root);
+ return b[0];
+ },
+ sum: function(items, begin, end, property, root) {
+ var value, sum, i;
+ for (sum = 0 , i = begin; i < end; ++i) {
+ value = items[i];
+ value = (root ? value[root] : value)[property];
+ sum += value;
+ }
+ return sum;
+ }
+ },
+ _eventToMethodMap: {
+ add: 'onCollectionAdd',
+ beforeitemchange: 'onCollectionBeforeItemChange',
+ beginupdate: 'onCollectionBeginUpdate',
+ endupdate: 'onCollectionEndUpdate',
+ itemchange: 'onCollectionItemChange',
+ filtereditemchange: 'onCollectionFilteredItemChange',
+ refresh: 'onCollectionRefresh',
+ remove: 'onCollectionRemove',
+ beforesort: 'beforeCollectionSort',
+ sort: 'onCollectionSort',
+ filter: 'onCollectionFilter',
+ filteradd: 'onCollectionFilterAdd',
+ updatekey: 'onCollectionUpdateKey'
+ },
+ /**
+ * Adds an observing object to this collection. Observers are given first view of all
+ * events that we may fire. For any event an observer may implement a method whose
+ * name starts with "onCollection" to receive the event. The `{@link #event-add add}`
+ * event for example would be passed to `"onCollectionAdd"`.
+ *
+ * The only restriction to observers is that they are not allowed to add or remove
+ * observers from inside these methods.
+ *
+ * @param {Ext.util.Collection} observer The observer instance.
+ * @private
+ * @since 5.0.0
+ */
+ addObserver: function(observer) {
+ var me = this,
+ observers = me.observers;
+ if (!observers) {
+ me.observers = observers = [];
+ }
+ if (Ext.Array.contains(observers, observer)) {
+ Ext.Error.raise('Observer already added');
+ }
+ // if we're in the middle of notifying, we need to clone the observers
+ if (me.notifying) {
+ me.observers = observers = observers.slice(0);
+ }
+ observers.push(observer);
+ if (observers.length > 1) {
+ // Allow observers to be inserted with a priority.
+ // For example GroupCollections must react to Collection mutation before views.
+ Ext.Array.sort(observers, me.prioritySortFn);
+ }
+ },
+ prioritySortFn: function(o1, o2) {
+ var a = o1.observerPriority || 0,
+ b = o2.observerPriority || 0;
+ return a - b;
+ },
+ applyExtraKeys: function(extraKeys, oldExtraKeys) {
+ var me = this,
+ ret = oldExtraKeys || {},
+ config, name, value;
+ for (name in extraKeys) {
+ value = extraKeys[name];
+ if (!value.isCollectionKey) {
+ config = {
+ collection: me
+ };
+ if (Ext.isString(value)) {
+ config.property = value;
+ } else {
+ config = Ext.apply(config, value);
+ }
+ value = new Ext.util.CollectionKey(config);
+ } else {
+ value.setCollection(me);
+ }
+ ret[name] = me[name] = value;
+ value.name = name;
+ }
+ return ret;
+ },
+ applyGrouper: function(grouper) {
+ if (grouper) {
+ grouper = this.getSorters().decodeSorter(grouper, Ext.util.Grouper);
+ }
+ return grouper;
+ },
+ /**
+ * Returns the items array on which to operate. This is called to handle the two
+ * possible forms used by various methods that accept items:
+ *
+ * collection.add(item1, item2, item3);
+ * collection.add([ item1, item2, item3 ]);
+ *
+ * Things get interesting when other arguments are involved:
+ *
+ * collection.insert(index, item1, item2, item3);
+ * collection.insert(index, [ item1, item2, item3 ]);
+ *
+ * As well as below because we have to distinguish the one item from from the array:
+ *
+ * collection.add(item);
+ * collection.insert(index, item);
+ *
+ * @param {Arguments} args The arguments object from the caller.
+ * @param {Number} index The index in `args` (the caller's arguments) of `items`.
+ * @return {Object[]} The array of items on which to operate.
+ * @private
+ * @since 5.0.0
+ */
+ decodeItems: function(args, index) {
+ var me = this,
+ ret = (index === undefined) ? args : args[index],
+ cloned, decoder, i;
+ if (!ret || !ret.$cloned) {
+ cloned = args.length > index + 1 || !Ext.isIterable(ret);
+ if (cloned) {
+ ret = Ext.Array.slice(args, index);
+ if (ret.length === 1 && ret[0] === undefined) {
+ ret.length = 0;
+ }
+ }
+ decoder = me.getDecoder();
+ if (decoder) {
+ if (!cloned) {
+ ret = ret.slice(0);
+ cloned = true;
+ }
+ for (i = ret.length; i-- > 0; ) {
+ if ((ret[i] = decoder.call(me, ret[i])) === false) {
+ ret.splice(i, 1);
+ }
+ }
+ }
+ if (cloned) {
+ ret.$cloned = true;
+ }
+ }
+ return ret;
+ },
+ /**
+ * Returns the map of key to index for all items in this collection. This method will
+ * lazily populate this map on request. This map is maintained when doing so does not
+ * involve too much overhead. When this threshold is cross, the index map is discarded
+ * and must be rebuilt by calling this method.
+ *
+ * @return {Object}
+ * @private
+ * @since 5.0.0
+ */
+ getIndices: function() {
+ var me = this,
+ indices = me.indices,
+ items = me.items,
+ n = items.length,
+ i, key;
+ if (!indices) {
+ me.indices = indices = {};
+ ++me.indexRebuilds;
+ for (i = 0; i < n; ++i) {
+ key = me.getKey(items[i]);
+ indices[key] = i;
+ }
+ }
+ return indices;
+ },
+ /**
+ * This method wraps all fired events and gives observers first view of the change.
+ *
+ * @param {String} eventName The name of the event to fire.
+ * @param {Array} [args] The event arguments. This collection instance is inserted at
+ * the front of this array if there is any receiver for the notification.
+ *
+ * @private
+ * @since 5.0.0
+ */
+ notify: function(eventName, args) {
+ var me = this,
+ observers = me.observers,
+ methodName = me._eventToMethodMap[eventName],
+ added = 0,
+ index, length, method, observer;
+ args = args || [];
+ if (observers && methodName) {
+ me.notifying = true;
+ for (index = 0 , length = observers.length; index < length; ++index) {
+ method = (observer = observers[index])[methodName];
+ if (method) {
+ if (!added++) {
+ // jshint ignore:line
+ args.unshift(me);
+ }
+ // put this Collection as the first argument
+ method.apply(observer, args);
+ }
+ }
+ me.notifying = false;
+ }
+ // During construction, no need to fire an event here
+ if (!me.hasListeners) {
+ return;
+ }
+ if (me.hasListeners[eventName]) {
+ if (!added) {
+ args.unshift(me);
+ }
+ // put this Collection as the first argument
+ me.fireEventArgs(eventName, args);
+ }
+ },
+ /**
+ * Returns the filter function.
+ * @return {Function} sortFn The sort function.
+ */
+ getFilterFn: function() {
+ return this.getFilters().getFilterFn();
+ },
+ /**
+ * Returns the `Ext.util.FilterCollection`. Unless `autoCreate` is explicitly passed
+ * as `false` this collection will be automatically created if it does not yet exist.
+ * @param [autoCreate=true] Pass `false` to disable auto-creation of the collection.
+ * @return {Ext.util.FilterCollection} The collection of filters.
+ */
+ getFilters: function(autoCreate) {
+ var ret = this._filters;
+ if (!ret && autoCreate !== false) {
+ ret = new Ext.util.FilterCollection({
+ id: this.getId() + '-filters' + (this.generation || '')
+ });
+ this.setFilters(ret);
+ }
+ return ret;
+ },
+ /**
+ * This method can be used to conveniently test whether an individual item would be
+ * removed due to the current filter.
+ * @param {Object} item The item to test.
+ * @return {Boolean} The value `true` if the item would be "removed" from the
+ * collection due to filters or `false` otherwise.
+ */
+ isItemFiltered: function(item) {
+ return !this.getFilters().filterFn(item);
+ },
+ /**
+ * Called after a change of the filter is complete.
+ *
+ * For example:
+ *
+ * onFilterChange: function (filters) {
+ * if (this.filtered) {
+ * // process filters
+ * } else {
+ * // no filters present
+ * }
+ * }
+ *
+ * @template
+ * @method
+ * @param {Ext.util.FilterCollection} filters The filters collection.
+ */
+ onFilterChange: function(filters) {
+ var me = this,
+ source = me.getSource(),
+ extraKeys, newKeys, key;
+ if (!source) {
+ // In this method, we have changed the filter but since we don't start with
+ // any and we create the source collection as needed that means we are getting
+ // our first filter.
+ extraKeys = me.getExtraKeys();
+ if (extraKeys) {
+ newKeys = {};
+ for (key in extraKeys) {
+ newKeys[key] = extraKeys[key].clone(me);
+ }
+ }
+ source = new Ext.util.Collection({
+ keyFn: me.getKey,
+ extraKeys: newKeys,
+ rootProperty: me.getRootProperty()
+ });
+ if (me.length) {
+ source.add(me.items);
+ }
+ me.setSource(source);
+ me.autoSource = source;
+ } else {
+ if (source.destroyed) {
+ return;
+ }
+ if (source.length || me.length) {
+ // if both us and the source are empty then we can skip this
+ me.onCollectionRefresh(source);
+ }
+ }
+ me.notify('filter');
+ },
+ //-------------------------------------------------------------------------
+ // Private
+ applyFilters: function(filters, collection) {
+ if (!filters || filters.isFilterCollection) {
+ return filters;
+ }
+ if (filters) {
+ if (!collection) {
+ collection = this.getFilters();
+ }
+ collection.splice(0, collection.length, filters);
+ }
+ return collection;
+ },
+ updateFilters: function(newFilters, oldFilters) {
+ var me = this;
+ if (oldFilters) {
+ // Do not disconnect from owning Filterable because
+ // default options (eg _rootProperty) are read from there.
+ // FilterCollections are detached from the Collection when the owning Store
+ // is remoteFilter: true or the owning store is a TreeStore and only filters
+ // new nodes before filling a parent node.
+ oldFilters.un('endupdate', 'onEndUpdateFilters', me);
+ }
+ if (newFilters) {
+ newFilters.on({
+ endupdate: 'onEndUpdateFilters',
+ scope: me,
+ priority: me.$endUpdatePriority
+ });
+ newFilters.$filterable = me;
+ }
+ me.onEndUpdateFilters(newFilters);
+ },
+ onEndUpdateFilters: function(filters) {
+ var me = this,
+ was = me.filtered,
+ is = !!filters && (filters.getFilterCount() > 0);
+ // booleanize filters
+ if (was || is) {
+ me.filtered = is;
+ me.onFilterChange(filters);
+ }
+ },
+ /**
+ * Returns an up to date sort function.
+ * @return {Function} The sort function.
+ */
+ getSortFn: function() {
+ return this._sortFn || this.createSortFn();
+ },
+ /**
+ * Returns the `Ext.util.SorterCollection`. Unless `autoCreate` is explicitly passed
+ * as `false` this collection will be automatically created if it does not yet exist.
+ * @param [autoCreate=true] Pass `false` to disable auto-creation of the collection.
+ * @return {Ext.util.SorterCollection} The collection of sorters.
+ */
+ getSorters: function(autoCreate) {
+ var ret = this._sorters;
+ if (!ret && autoCreate !== false) {
+ ret = new Ext.util.SorterCollection({
+ id: this.getId() + '-sorters' + (this.generation || '')
+ });
+ this.setSorters(ret);
+ }
+ return ret;
+ },
+ /**
+ * Called after a change of the sort is complete.
+ *
+ * For example:
+ *
+ * onSortChange: function (sorters) {
+ * if (this.sorted) {
+ * // process sorters
+ * } else {
+ * // no sorters present
+ * }
+ * }
+ *
+ * @template
+ * @method
+ * @param {Ext.util.SorterCollection} sorters The sorters collection.
+ */
+ onSortChange: function() {
+ if (this.sorted) {
+ this.sortItems();
+ }
+ },
+ /**
+ * Updates the sorters collection and triggers sorting of this Sortable.
+ *
+ * For example:
+ *
+ * //sort by a single field
+ * myStore.sort('myField', 'DESC');
+ *
+ * //sorting by multiple fields
+ * myStore.sort([{
+ * property : 'age',
+ * direction: 'ASC'
+ * }, {
+ * property : 'name',
+ * direction: 'DESC'
+ * }]);
+ *
+ * When passing a single string argument to sort, the `direction` is maintained for
+ * each field and is toggled automatically. So this code:
+ *
+ * store.sort('myField');
+ * store.sort('myField');
+ *
+ * Is equivalent to the following:
+ *
+ * store.sort('myField', 'ASC');
+ * store.sort('myField', 'DESC');
+ *
+ * @param {String/Function/Ext.util.Sorter[]} [property] Either the name of a property
+ * (such as a field of a `Ext.data.Model` in a `Store`), an array of configurations
+ * for `Ext.util.Sorter` instances or just a comparison function.
+ * @param {String} [direction] The direction by which to sort the data. This parameter
+ * is only valid when `property` is a String, otherwise the second parameter is the
+ * `mode`.
+ * @param {String} [mode="replace"] Where to put new sorters in the collection. This
+ * should be one the following values:
+ *
+ * - **`replace`** : The new sorter(s) become the sole sorter set for this Sortable.
+ * This is the most useful call mode to programmatically sort by multiple fields.
+ *
+ * - **`prepend`** : The new sorters are inserted as the primary sorters. The sorter
+ * collection length must be controlled by the developer.
+ *
+ * - **`multi`** : Similar to **`prepend`** the new sorters are inserted at the front
+ * of the collection of sorters. Following the insertion, however, this mode trims
+ * the sorter collection to enforce the `multiSortLimit` config. This is useful for
+ * implementing intuitive "Sort by this" user interfaces.
+ *
+ * - **`append`** : The new sorters are added at the end of the collection.
+ * @return {Ext.util.Collection} This instance.
+ */
+ sort: function(property, direction, mode) {
+ var sorters = this.getSorters();
+ sorters.addSort.apply(sorters, arguments);
+ return this;
+ },
+ /**
+ * This method will sort an array based on the currently configured {@link #sorters}.
+ * @param {Array} data The array you want to have sorted.
+ * @return {Array} The array you passed after it is sorted.
+ */
+ sortData: function(data) {
+ Ext.Array.sort(data, this.getSortFn());
+ return data;
+ },
+ /**
+ * Sorts the items of the collection using the supplied function. This should only be
+ * called for collections that have no `sorters` defined.
+ * @param {Function} sortFn The function by which to sort the items.
+ * @since 5.0.0
+ */
+ sortItems: function(sortFn) {
+ var me = this;
+ if (me.sorted) {
+ if (sortFn) {
+ Ext.raise('Collections with sorters cannot resorted');
+ }
+ sortFn = me.getSortFn();
+ }
+ me.indices = null;
+ me.notify('beforesort', [
+ me.getSorters(false)
+ ]);
+ if (me.length) {
+ Ext.Array.sort(me.items, sortFn);
+ }
+ // Even if there's no data, notify interested parties.
+ // Eg: Stores must react and fire their refresh and sort events.
+ me.notify('sort');
+ },
+ /**
+ * Sorts the collection by a single sorter function
+ * @param {Function} sortFn The function to sort by
+ * @deprecated 6.5.0 This method is deprecated.
+ */
+ sortBy: function(sortFn) {
+ return this.sortItems(sortFn);
+ },
+ /**
+ * @private
+ * Can be called to find the insertion index of a passed object in this collection.
+ * Or can be passed an items array to search in, and may be passed a comparator
+ */
+ findInsertionIndex: function(item, items, comparatorFn, index) {
+ return Ext.Array.findInsertionIndex(item, items || this.items, comparatorFn || this.getSortFn(), index);
+ },
+ applySorters: function(sorters, collection) {
+ if (!sorters || sorters.isSorterCollection) {
+ return sorters;
+ }
+ if (sorters) {
+ if (!collection) {
+ collection = this.getSorters();
+ }
+ collection.splice(0, collection.length, sorters);
+ }
+ return collection;
+ },
+ createSortFn: function() {
+ var me = this,
+ grouper = me.getGrouper(),
+ sorters = me.getSorters(false),
+ sorterFn = sorters ? sorters.getSortFn() : null;
+ if (!grouper) {
+ return sorterFn;
+ }
+ return function(lhs, rhs) {
+ var ret = grouper.sort(lhs, rhs);
+ if (!ret && sorterFn) {
+ ret = sorterFn(lhs, rhs);
+ }
+ return ret;
+ };
+ },
+ updateGrouper: function(grouper) {
+ var me = this,
+ groups = me.getGroups(),
+ sorters = me.getSorters(),
+ populate;
+ me.onSorterChange();
+ me.grouped = !!grouper;
+ if (grouper) {
+ if (me.getTrackGroups()) {
+ if (!groups) {
+ groups = new Ext.util.GroupCollection({
+ id: me.getId() + '-groups' + (me.generation || ''),
+ itemRoot: me.getRootProperty(),
+ groupConfig: me.getGroupConfig()
+ });
+ groups.$groupable = me;
+ me.setGroups(groups);
+ }
+ groups.setGrouper(grouper);
+ populate = true;
+ }
+ } else {
+ if (groups) {
+ me.removeObserver(groups);
+ groups.destroy();
+ }
+ me.setGroups(null);
+ }
+ if (!sorters.updating) {
+ me.onEndUpdateSorters(sorters);
+ }
+ if (populate) {
+ groups.onCollectionRefresh(me);
+ }
+ },
+ updateSorters: function(newSorters, oldSorters) {
+ var me = this;
+ if (oldSorters && !oldSorters.destroyed) {
+ // Do not disconnect from owning Filterable because
+ // default options (eg _rootProperty) are read from there.
+ // SorterCollections are detached from the Collection when the owning Store
+ // is remoteSort: true or the owning store is a TreeStore and only sorts
+ // new nodes before filling a parent node.
+ oldSorters.un('endupdate', 'onEndUpdateSorters', me);
+ }
+ if (newSorters) {
+ newSorters.on({
+ endupdate: 'onEndUpdateSorters',
+ scope: me,
+ priority: me.$endUpdatePriority
+ });
+ if (me.manageSorters) {
+ newSorters.$sortable = me;
+ }
+ }
+ me.onSorterChange();
+ me.onEndUpdateSorters(newSorters);
+ },
+ onSorterChange: function() {
+ this._sortFn = null;
+ },
+ onEndUpdateSorters: function(sorters) {
+ var me = this,
+ was = me.sorted,
+ is = (me.grouped && me.getAutoGroup()) || (sorters && sorters.length > 0);
+ if (was || is) {
+ // ensure flag property is a boolean.
+ // sorters && (sorters.length > 0) may evaluate to null
+ me.sorted = !!is;
+ me.onSortChange(sorters);
+ }
+ },
+ /**
+ * Removes an observing object to this collection. See `addObserver` for details.
+ *
+ * @param {Ext.util.Collection} observer The observer instance.
+ * @private
+ * @since 5.0.0
+ */
+ removeObserver: function(observer) {
+ var observers = this.observers;
+ if (observers) {
+ Ext.Array.remove(observers, observer);
+ }
+ },
+ /**
+ * This method is what you might find in the core of a merge sort. We have an items
+ * array that is sorted so we sort the newItems and merge the two sorted arrays. In
+ * the general case, newItems will be no larger than all items so sorting it will be
+ * faster than simply concatenating the arrays and calling sort on it.
+ *
+ * We take advantage of the nature of this process to generate add events as ranges.
+ *
+ * @param {Object[]} newItems
+ * @param {Object[]} newKeys
+ * @private
+ * @since 5.0.0
+ */
+ spliceMerge: function(newItems, newKeys) {
+ var me = this,
+ map = me.map,
+ newLength = newItems.length,
+ oldIndex = 0,
+ oldItems = me.items,
+ oldLength = oldItems.length,
+ adds = [],
+ count = 0,
+ items = [],
+ sortFn = me.getSortFn(),
+ // account for grouper and sorter(s)
+ addItems, end, i, newItem, oldItem, newIndex;
+ me.items = items;
+ //
+ // oldItems
+ // +----+----+----+----+
+ // | 15 | 25 | 35 | 45 |
+ // +----+----+----+----+
+ // 0 1 2 3
+ //
+ // newItems
+ // +----+----+----+----+----+----+
+ // | 10 | 11 | 20 | 21 | 50 | 51 |
+ // +----+----+----+----+----+----+
+ // 0 1 2 3 4 5
+ //
+ for (newIndex = 0; newIndex < newLength; newIndex = end) {
+ newItem = newItems[newIndex];
+ // Flush out any oldItems that are <= newItem
+ for (; oldIndex < oldLength; ++oldIndex) {
+ // Consider above arrays...
+ // at newIndex == 0 this loop sets oldItem but breaks immediately
+ // at newIndex == 2 this loop pushes 15 and breaks w/oldIndex=1
+ // at newIndex == 4 this loop pushes 25, 35 and 45 and breaks w/oldIndex=4
+ if (sortFn(newItem, oldItem = oldItems[oldIndex]) < 0) {
+ break;
+ }
+ items.push(oldItem);
+ }
+ if (oldIndex === oldLength) {
+ // Consider above arrays...
+ // at newIndex == 0 we won't come in here (oldIndex == 0)
+ // at newIndex == 2 we won't come in here (oldIndex == 1)
+ // at newIndex == 4 we come here (oldIndex == 4), push 50 & 51 and break
+ adds[count++] = {
+ at: items.length,
+ itemAt: items[items.length - 1],
+ items: (addItems = [])
+ };
+ if (count > 1) {
+ adds[count - 2].next = adds[count - 1];
+ }
+ for (; newIndex < newLength; ++newIndex) {
+ addItems.push(newItem = newItems[newIndex]);
+ items.push(newItem);
+ }
+ break;
+ }
+ // else oldItem is the item from oldItems that is > newItem
+ // Consider above arrays...
+ // at newIndex == 0 we will push 10
+ // at newIndex == 2 we will push 20
+ adds[count++] = {
+ at: items.length,
+ itemAt: items[items.length - 1],
+ items: (addItems = [
+ newItem
+ ])
+ };
+ if (count > 1) {
+ adds[count - 2].next = adds[count - 1];
+ }
+ items.push(newItem);
+ for (end = newIndex + 1; end < newLength; ++end) {
+ // Consider above arrays...
+ // at newIndex == 0 this loop pushes 11 then breaks w/end == 2
+ // at newIndex == 2 this loop pushes 21 the breaks w/end == 4
+ if (sortFn(newItem = newItems[end], oldItem) >= 0) {
+ break;
+ }
+ items.push(newItem);
+ addItems.push(newItem);
+ }
+ }
+ // if oldItems had 55 as its final element, the above loop would have pushed
+ // all of newItems so the outer for loop would then fall out
+ for (; oldIndex < oldLength; ++oldIndex) {
+ // In the above example, we won't come in here, but if you imagine a 55 in
+ // oldItems we would have oldIndex == 4 and oldLength == 5
+ items.push(oldItems[oldIndex]);
+ }
+ for (i = 0; i < newLength; ++i) {
+ map[newKeys[i]] = newItems[i];
+ }
+ me.length = items.length;
+ ++me.generation;
+ me.indices = null;
+ // Tell users of the adds in increasing index order.
+ for (i = 0; i < count; ++i) {
+ me.notify('add', [
+ adds[i]
+ ]);
+ }
+ },
+ transformItems: Ext.identityFn,
+ getGroups: function() {
+ return this.callParent() || null;
+ },
+ updateAutoGroup: function(autoGroup) {
+ var groups = this.getGroups();
+ if (groups) {
+ groups.setAutoGroup(autoGroup);
+ }
+ // Important to call this so it can clear the .sorted flag
+ // as needed
+ this.onEndUpdateSorters(this._sorters);
+ },
+ updateGroups: function(newGroups, oldGroups) {
+ if (oldGroups) {
+ this.removeObserver(oldGroups);
+ }
+ if (newGroups) {
+ this.addObserver(newGroups);
+ }
+ },
+ updateSource: function(newSource, oldSource) {
+ var me = this,
+ auto = me.autoSource;
+ if (oldSource) {
+ if (!oldSource.destroyed) {
+ oldSource.removeObserver(me);
+ }
+ if (oldSource === auto) {
+ auto.destroy();
+ me.autoSource = null;
+ }
+ }
+ if (newSource) {
+ newSource.addObserver(me);
+ if (newSource.length || me.length) {
+ me.onCollectionRefresh(newSource);
+ }
+ }
+ }
+}, function() {
+ var prototype = this.prototype;
+ // Minor compat method
+ prototype.removeAtKey = prototype.removeByKey;
+ /**
+ * This method is an alias for `decodeItems` but is called when items are being
+ * removed. If a `decoder` is provided it may be necessary to also override this
+ * method to achieve symmetry between adding and removing items. This is the case
+ * for `Ext.util.FilterCollection' and `Ext.util.SorterCollection' for example.
+ *
+ * @method decodeRemoveItems
+ * @protected
+ * @since 5.0.0
+ */
+ prototype.decodeRemoveItems = prototype.decodeItems;
+ Ext.Object.each(prototype._aggregators, function(name) {
+ prototype[name] = function(property, begin, end) {
+ return this.aggregate(property, name, begin, end);
+ };
+ prototype[name + 'ByGroup'] = function(property) {
+ return this.aggregateByGroup(property, name);
+ };
+ });
+});
+
+/**
+ * This class provides access to a range of records in a {@link Ext.data.Store store}.
+ * Instances of this class are not created directly but are rather returned by a store's
+ * {@link Ext.data.AbstractStore#createActiveRange createActiveRange} method.
+ *
+ * This class is useful for buffered rendering where only a portion of the total set of
+ * records are needed. By passing that information to a `Range`, the access to records
+ * can be abstracted even across {@link Ext.data.virtual.Store virtual stores} where
+ * only those records needed by views are fetched from the server.
+ * @since 6.5.0
+ */
+Ext.define('Ext.data.Range', {
+ isDataRange: true,
+ /**
+ * @cfg {Number} begin
+ * The first record index of interest.
+ *
+ * This property is set by the `goto` method and is stored on the instance for
+ * readonly use.
+ * @readonly
+ */
+ begin: 0,
+ /**
+ * @cfg {Number} buffer
+ * The buffer to execute server requests.
+ */
+ buffer: 0,
+ /**
+ * @cfg {Number} end
+ * The first record beyond the range of interest. This is to make "length" of the
+ * range simply `end - begin`.
+ *
+ * This property is set by the `goto` method and is stored on the instance for
+ * readonly use.
+ */
+ end: 0,
+ /**
+ * @property (Number} length
+ * The number of records in the range of `[begin, end)`. This is equal to the
+ * difference `end - begin`.
+ *
+ * This property is maintained by the `goto` method and is stored on the instance for
+ * readonly use.
+ * @readonly
+ */
+ length: 0,
+ /**
+ * @property {Ext.data.Model[]} records
+ * The records corresponding to the `begin` and `end` of this range. For normal
+ * stores this is the standard array of records.
+ *
+ * For a {@link Ext.data.virtual.Store virtual store} this is a sparse object of
+ * available records bounded by the limits of this range.
+ *
+ * In all cases, this object is keyed by the record index and (except for the
+ * `length` property) should be treated as an array.
+ * @readonly
+ */
+ /**
+ * @cfg {Ext.data.AbstractStore} store
+ * The associated store. This config must be supplied at construction and cannot
+ * be changed after that time.
+ * @readonly
+ */
+ store: null,
+ constructor: function(config) {
+ var me = this,
+ activeRanges, store;
+ Ext.apply(me, config);
+ store = me.store;
+ if (!(activeRanges = store.activeRanges)) {
+ store.activeRanges = activeRanges = [];
+ }
+ activeRanges.push(me);
+ me.refresh();
+ if ('begin' in config) {
+ me.begin = me.end = 0;
+ // Applied on us above, so clear it
+ /* eslint-disable-next-line dot-notation */
+ me.goto(config.begin, config.end);
+ }
+ },
+ destroy: function() {
+ var me = this,
+ store = me.store,
+ activeRanges = store && store.activeRanges;
+ Ext.destroy(me.storeListeners);
+ if (activeRanges) {
+ Ext.Array.remove(activeRanges, me);
+ }
+ me.callParent();
+ },
+ "goto": function(begin, end) {
+ var me = this,
+ buffer = me.buffer,
+ task = me.task;
+ me.begin = begin;
+ me.end = end;
+ me.length = end - begin;
+ if (buffer > 0) {
+ if (!task) {
+ me.task = task = new Ext.util.DelayedTask(me.doGoto, me);
+ }
+ task.delay(buffer);
+ } else {
+ me.doGoto();
+ }
+ },
+ privates: {
+ lastBegin: 0,
+ lastEnd: 0,
+ doGoto: Ext.privateFn,
+ refresh: function() {
+ this.records = this.store.getData().items;
+ }
+ }
+});
+
+/**
+ * This class accepts an object that serves as a template for creating new objects. Like
+ * other templates (`Ext.XTemplate`) this creation step requires a context object to give
+ * the template its values.
+ *
+ * For example:
+ *
+ * var tpl = new Ext.util.ObjectTemplate({
+ * property: 'Hello {name}',
+ * data: {
+ * value: '{age}'
+ * }
+ * });
+ *
+ * var obj = tpl.apply({
+ * name: 'Bill',
+ * age: 42
+ * });
+ *
+ * // obj = {
+ * // property: 'Hello Bill',
+ * // data: {
+ * // value: 42
+ * // }
+ * // }
+ *
+ * @since 5.0.0
+ */
+Ext.define('Ext.util.ObjectTemplate', {
+ isObjectTemplate: true,
+ excludeProperties: {},
+ valueRe: /^[{][a-z.]+[}]$/i,
+ statics: {
+ /**
+ * Creates an `ObjectTemplate` given a config object or instance.
+ * @param {Object/Ext.util.ObjectTemplate} template The template object.
+ * @param {Object} [options]
+ * @return {Ext.util.ObjectTemplate}
+ * @since 5.0.0
+ */
+ create: function(template, options) {
+ if (!Ext.isObject(template)) {
+ Ext.raise('The template is not an Object');
+ }
+ return template.isObjectTemplate ? template : new Ext.util.ObjectTemplate(template, options);
+ }
+ },
+ /**
+ * Constructs the `ObjectTemplate`. The actual compilation of the object to a ready to
+ * apply form happens on the first call to `apply`.
+ * @param {Object} template
+ * @param {Object} [options]
+ * @since 5.0.0
+ */
+ constructor: function(template, options) {
+ Ext.apply(this, options);
+ this.template = template;
+ },
+ /**
+ * Applies the given `context` object to this template and returns a new object with
+ * the appropriate pieces replaced.
+ * @param {Object} context The data used to populate the template.
+ * @return {Object}
+ * @since 5.0.0
+ */
+ apply: function(context) {
+ var me = this;
+ delete me.apply;
+ me.apply = me.compile(me.template);
+ return me.apply(context);
+ },
+ privates: {
+ /**
+ * Compiles the given template into an `apply` method that is ready to run. This
+ * method is used recursively to process object properties and array elements.
+ * @param {Mixed} template
+ * @return {Function}
+ * @since 5.0.0
+ */
+ compile: function(template) {
+ var me = this,
+ exclude = me.excludeProperties,
+ compiled, i, len, fn;
+ // TODO: loops over array or objects
+ if (Ext.isString(template)) {
+ if (template.indexOf('{') < 0) {
+ fn = function() {
+ return template;
+ };
+ } else if (me.valueRe.test(template)) {
+ template = template.substring(1, template.length - 1).split('.');
+ fn = function(context) {
+ var v, i;
+ for (v = context , i = 0; v && i < template.length; ++i) {
+ v = v[template[i]];
+ }
+ return v;
+ };
+ } else {
+ template = new Ext.XTemplate(template);
+ fn = function(context) {
+ return template.apply(context);
+ };
+ }
+ } else if (!template || Ext.isPrimitive(template) || Ext.isFunction(template)) {
+ fn = function() {
+ return template;
+ };
+ } else if (template instanceof Array) {
+ compiled = [];
+ for (i = 0 , len = template.length; i < len; ++i) {
+ compiled[i] = me.compile(template[i]);
+ }
+ fn = function(context) {
+ var ret = [],
+ i;
+ for (i = 0; i < len; ++i) {
+ ret[i] = compiled[i](context);
+ }
+ return ret;
+ };
+ } else {
+ compiled = {};
+ for (i in template) {
+ if (!exclude[i]) {
+ compiled[i] = me.compile(template[i]);
+ }
+ }
+ fn = function(context) {
+ var ret = {},
+ i, v;
+ for (i in template) {
+ v = exclude[i] ? template[i] : compiled[i](context);
+ if (v !== undefined) {
+ ret[i] = v;
+ }
+ }
+ return ret;
+ };
+ }
+ return fn;
+ }
+ }
+});
+
+/**
+ * @private
+ */
+Ext.define('Ext.data.schema.Role', {
+ /**
+ * @property {Ext.data.schema.Association} association
+ * @readonly
+ */
+ isRole: true,
+ /**
+ * @property {Boolean} left
+ * @readonly
+ */
+ left: true,
+ /**
+ * @property {Boolean} owner
+ * @readonly
+ */
+ owner: false,
+ /**
+ * @property {String} side
+ * @readonly
+ */
+ side: 'left',
+ /**
+ * @property {Boolean} isMany
+ * @readonly
+ */
+ isMany: false,
+ /**
+ * @property {Ext.Class} cls
+ * The `Ext.data.Model` derived class.
+ * @readonly
+ */
+ /**
+ * @property {Ext.data.schema.Role} inverse
+ * @readonly
+ */
+ /**
+ * @property {String} type
+ * The `{@link Ext.data.Model#entityName}` derived class.
+ * @readonly
+ */
+ /**
+ * @property {String} role
+ * @readonly
+ */
+ defaultReaderType: 'json',
+ _internalReadOptions: {
+ recordsOnly: true,
+ asRoot: true
+ },
+ constructor: function(association, config) {
+ var me = this,
+ extra = config.extra;
+ Ext.apply(me, config);
+ if (extra) {
+ extra = Ext.apply({}, extra);
+ delete extra.type;
+ Ext.apply(me, extra);
+ delete me.extra;
+ }
+ me.association = association;
+ // The Association's owner property starts as either "left" or "right" (a string)
+ // and we promote it to a reference to the appropriate Role instance here.
+ if (association.owner === me.side) {
+ association.owner = me;
+ me.owner = true;
+ }
+ },
+ processUpdate: function() {
+ Ext.raise('Only the "many" for an association may be processed. "' + this.role + '" is not valid.');
+ },
+ /**
+ * Exclude any locally modified records that don't belong in the store. Include locally
+ * modified records that should be in the store. Also correct any foreign keys that
+ * need to be updated.
+ *
+ * @param {Ext.data.Store} store The store.
+ * @param {Ext.data.Model} associatedEntity The entity that owns the records.
+ * @param {Ext.data.Model[]} records The records to check.
+ * @param {Ext.data.Session} session The session holding the records
+ * @return {Ext.data.Model[]} The corrected set of records.
+ *
+ * @private
+ */
+ processLoad: function(store, associatedEntity, records, session) {
+ return records;
+ },
+ /**
+ * @method
+ *
+ * Check whether a record belongs to any stores when it is added to the session.
+ *
+ * @param {Ext.data.Session} session The session
+ * @param {Ext.data.Model} record The model being added to the session
+ * @private
+ */
+ checkMembership: Ext.emptyFn,
+ /**
+ * Adopt the associated items when a record is adopted.
+ * @param {Ext.data.Model} record The record being adopted.
+ * @param {Ext.data.Session} session The session being adopted into
+ *
+ * @private
+ */
+ adoptAssociated: function(record, session) {
+ var other = this.getAssociatedItem(record);
+ if (other) {
+ session.adopt(other);
+ }
+ },
+ $roleFilterId: '$associationRoleFilter',
+ createAssociationStore: function(session, from, records, isComplete) {
+ var me = this,
+ association = me.association,
+ foreignKeyName = association.getFieldName(),
+ isMany = association.isManyToMany,
+ storeConfig = me.storeConfig,
+ id = from.getId(),
+ config = {
+ // Always want immediate load
+ asynchronousLoad: false,
+ model: me.cls,
+ role: me,
+ session: session,
+ associatedEntity: from,
+ disableMetaChangeEvent: true,
+ pageSize: null,
+ remoteFilter: true,
+ trackRemoved: !session
+ },
+ store;
+ if (isMany) {
+ // For many-to-many associations each role has a field
+ config.filters = [
+ {
+ id: me.$roleFilterId,
+ property: me.inverse.field,
+ // @TODO filterProperty
+ value: id,
+ exactMatch: true
+ }
+ ];
+ } else if (foreignKeyName) {
+ config.filters = [
+ {
+ id: me.$roleFilterId,
+ property: foreignKeyName,
+ // @TODO filterProperty
+ value: id,
+ exactMatch: true
+ }
+ ];
+ config.foreignKeyName = foreignKeyName;
+ }
+ if (storeConfig) {
+ Ext.apply(config, storeConfig);
+ }
+ store = Ext.Factory.store(config);
+ me.onStoreCreate(store, session, id);
+ // Want to run these in all cases for M-1, only with a session M-M
+ if (!isMany || session) {
+ store.on({
+ scope: me,
+ add: 'onAddToMany',
+ remove: 'onRemoveFromMany',
+ clear: 'onRemoveFromMany'
+ });
+ }
+ if (records) {
+ store.loadData(records);
+ }
+ store.complete = !!isComplete;
+ return store;
+ },
+ onStoreCreate: Ext.emptyFn,
+ getAssociatedStore: function(inverseRecord, options, scope, records, allowInfer) {
+ // Consider the Comment entity with a ticketId to a Ticket entity. The Comment
+ // is on the left (the FK holder's side) so we are implementing the guts of
+ // the comments() method to load the Store of Comment entities. This trek
+ // begins from a Ticket (inverseRecord).
+ var me = this,
+ storeName = me.getStoreName(),
+ store = inverseRecord[storeName],
+ hadStore = store,
+ session = inverseRecord.session,
+ load = options && options.reload,
+ source = inverseRecord.$source,
+ isComplete = false,
+ phantom = false,
+ hadSourceStore, args, i, len, raw, rec, sourceStore, hadRecords, isLoading;
+ if (!store) {
+ if (session) {
+ // We want to check whether we can automatically get the store contents from the
+ // parent session. For this to occur, we need to have a parent in the session,
+ // and the store needs to be created and loaded with the initial dataset.
+ if (source) {
+ phantom = source.phantom;
+ }
+ if (!records && source) {
+ sourceStore = source[storeName];
+ if (sourceStore && !sourceStore.isLoading()) {
+ records = [];
+ raw = sourceStore.getData().items;
+ for (i = 0 , len = raw.length; i < len; ++i) {
+ rec = raw[i];
+ records.push(session.getRecord(rec.self, rec.id));
+ }
+ isComplete = !!sourceStore.complete;
+ hadSourceStore = true;
+ }
+ }
+ if (!hadSourceStore) {
+ // We'll only hit here if we didn't have a usable source
+ hadRecords = !!records;
+ records = me.findRecords(session, inverseRecord, records, allowInfer);
+ if (!hadRecords && (!records || !records.length)) {
+ records = null;
+ }
+ isComplete = phantom || hadRecords;
+ }
+ } else {
+ // As long as we had the collection exist, we're complete, even if it's empty.
+ isComplete = !!records;
+ }
+ // If the inverse is a phantom, we can't be loading any data so we're complete
+ store = me.createAssociationStore(session, inverseRecord, records, isComplete || inverseRecord.phantom);
+ store.$source = sourceStore;
+ if (!records && (me.autoLoad || options)) {
+ load = true;
+ }
+ inverseRecord[storeName] = store;
+ }
+ if (options) {
+ // We need to trigger a load or the store is already loading. Defer
+ // callbacks until that happens
+ if (load || store.isLoading()) {
+ store.on('load', function(store, records, success, operation) {
+ args = [
+ store,
+ operation
+ ];
+ scope = scope || options.scope || inverseRecord;
+ if (success) {
+ Ext.callback(options.success, scope, args);
+ } else {
+ Ext.callback(options.failure, scope, args);
+ }
+ args.push(success);
+ Ext.callback(options, scope, args);
+ Ext.callback(options.callback, scope, args);
+ }, null, {
+ single: true
+ });
+ } else {
+ // Trigger straight away
+ args = [
+ store,
+ null
+ ];
+ scope = scope || options.scope || inverseRecord;
+ Ext.callback(options.success, scope, args);
+ args.push(true);
+ Ext.callback(options, scope, args);
+ Ext.callback(options.callback, scope, args);
+ }
+ }
+ isLoading = store.isLoading();
+ if (load) {
+ if (!isLoading) {
+ store.load();
+ }
+ } else if (hadStore && records && !isLoading) {
+ store.loadData(records);
+ }
+ return store;
+ },
+ /**
+ * Gets the store/record associated with this role from an existing record.
+ * Will only return if the value is loaded.
+ *
+ * @param {Ext.data.Model} rec The record
+ *
+ * @return {Ext.data.Model/Ext.data.Store} The associated item. `null` if not loaded.
+ * @private
+ */
+ getAssociatedItem: function(rec) {
+ var key = this.isMany ? this.getStoreName() : this.getInstanceName();
+ return rec[key] || null;
+ },
+ onDrop: Ext.emptyFn,
+ onIdChanged: Ext.emptyFn,
+ getReaderRoot: function() {
+ var me = this;
+ return me.associationKey || (me.associationKey = me.association.schema.getNamer().readerRoot(me.role));
+ },
+ getReader: function() {
+ var me = this,
+ reader = me.reader,
+ Model = me.cls,
+ useSimpleAccessors = !me.associationKey,
+ root = this.getReaderRoot();
+ if (reader && !reader.isReader) {
+ if (Ext.isString(reader)) {
+ reader = {
+ type: reader
+ };
+ }
+ Ext.applyIf(reader, {
+ model: Model,
+ rootProperty: root,
+ useSimpleAccessors: useSimpleAccessors,
+ type: me.defaultReaderType
+ });
+ reader = me.reader = Ext.createByAlias('reader.' + reader.type, reader);
+ }
+ return reader;
+ },
+ getInstanceName: function() {
+ var me = this;
+ return me.instanceName || (me.instanceName = me.association.schema.getNamer().instanceName(me.role));
+ },
+ getOldInstanceName: function() {
+ return this.oldInstanceName || (this.oldInstanceName = '$old' + this.getInstanceName());
+ },
+ getStoreName: function() {
+ var me = this;
+ return me.storeName || (me.storeName = me.association.schema.getNamer().storeName(me.role));
+ },
+ constructReader: function(fromReader) {
+ var me = this,
+ reader = me.getReader(),
+ Model = me.cls,
+ useSimpleAccessors = !me.associationKey,
+ root = me.getReaderRoot(),
+ proxyReader, proxy;
+ // No reader supplied
+ if (!reader) {
+ proxy = Model.getProxy();
+ // if the associated model has a Reader already, use that, otherwise attempt to
+ // create a sensible one
+ if (proxy) {
+ proxyReader = proxy.getReader();
+ reader = new proxyReader.self();
+ reader.copyFrom(proxyReader);
+ reader.setRootProperty(root);
+ } else {
+ reader = new fromReader.self({
+ model: Model,
+ useSimpleAccessors: useSimpleAccessors,
+ rootProperty: root
+ });
+ }
+ me.reader = reader;
+ }
+ return reader;
+ },
+ read: function(record, data, fromReader, readOptions) {
+ var reader = this.constructReader(fromReader),
+ root = reader.getRoot(data);
+ if (root) {
+ return reader.readRecords(root, readOptions, this._internalReadOptions);
+ }
+ },
+ getCallbackOptions: function(options, scope, defaultScope) {
+ if (typeof options === 'function') {
+ options = {
+ callback: options,
+ scope: scope || defaultScope
+ };
+ } else if (options) {
+ options = Ext.apply({}, options);
+ options.scope = scope || options.scope || defaultScope;
+ }
+ return options;
+ },
+ doGetFK: function(leftRecord, options, scope) {
+ // Consider the Department entity with a managerId to a User entity. This method
+ // is the guts of the getManager method that we place on the Department entity to
+ // acquire a User entity. We are the "manager" role and that role describes a
+ // User. This method is called, however, given a Department (leftRecord) as the
+ // start of this trek.
+ var me = this,
+ // the "manager" role
+ cls = me.cls,
+ // User
+ foreignKey = me.association.getFieldName(),
+ // "managerId"
+ instanceName = me.getInstanceName(),
+ // "manager"
+ rightRecord = leftRecord[instanceName],
+ // = department.manager
+ reload = options && options.reload,
+ done = rightRecord !== undefined && !reload,
+ session = leftRecord.session,
+ foreignKeyId, args;
+ if (!done) {
+ // We don't have the User record yet, so try to get it.
+ if (session) {
+ foreignKeyId = leftRecord.get(foreignKey);
+ if (foreignKeyId || foreignKeyId === 0) {
+ done = session.peekRecord(cls, foreignKeyId, true) && !reload;
+ rightRecord = session.getRecord(cls, foreignKeyId, false);
+ } else {
+ done = true;
+ leftRecord[instanceName] = rightRecord = null;
+ }
+ } else if (foreignKey) {
+ // The good news is that we do indeed have a FK so we can do a load using
+ // the value of the FK.
+ foreignKeyId = leftRecord.get(foreignKey);
+ if (!foreignKeyId && foreignKeyId !== 0) {
+ // A value of null ends that hope though... but we still need to do
+ // some callbacks perhaps.
+ done = true;
+ leftRecord[instanceName] = rightRecord = null;
+ } else {
+ // foreignKeyId is the managerId from the Department (record), so
+ // make a new User, set its idProperty and load the real record via
+ // User.load method.
+ if (!rightRecord) {
+ // We may be reloading, let's check if we have one.
+ rightRecord = cls.createWithId(foreignKeyId);
+ }
+ }
+ } else // we are not done in this case, so don't set "done"
+ {
+ // Without a FK value by which to request the User record, we cannot do
+ // anything. Declare victory and get out.
+ done = true;
+ rightRecord = null;
+ }
+ } else if (rightRecord) {
+ // If we're still loading, call load again which will handle the extra callbacks.
+ done = !rightRecord.isLoading();
+ }
+ if (done) {
+ if (options) {
+ args = [
+ rightRecord,
+ null
+ ];
+ scope = scope || options.scope || leftRecord;
+ Ext.callback(options.success, scope, args);
+ args.push(true);
+ Ext.callback(options, scope, args);
+ Ext.callback(options.callback, scope, args);
+ }
+ } else {
+ leftRecord[instanceName] = rightRecord;
+ options = me.getCallbackOptions(options, scope, leftRecord);
+ rightRecord.load(options);
+ }
+ return rightRecord;
+ },
+ doSetFK: function(leftRecord, rightRecord, options, scope) {
+ // Consider the Department entity with a managerId to a User entity. This method
+ // is the guts of the setManager method that we place on the Department entity to
+ // store the User entity. We are the "manager" role and that role describes a
+ // User. This method is called, however, given a Department (record) and the User
+ // (value).
+ var me = this,
+ foreignKey = me.association.getFieldName(),
+ // "managerId"
+ instanceName = me.getInstanceName(),
+ // "manager"
+ current = leftRecord[instanceName],
+ inverse = me.inverse,
+ inverseSetter = inverse.setterName,
+ // setManagerDepartment for User
+ modified, oldInstanceName;
+ if (rightRecord && rightRecord.isEntity) {
+ if (current !== rightRecord) {
+ oldInstanceName = me.getOldInstanceName();
+ leftRecord[oldInstanceName] = current;
+ leftRecord[instanceName] = rightRecord;
+ if (current && current.isEntity) {
+ current[inverse.getInstanceName()] = undefined;
+ }
+ if (foreignKey) {
+ leftRecord.set(foreignKey, rightRecord.getId());
+ }
+ delete leftRecord[oldInstanceName];
+ leftRecord.onAssociatedRecordSet(rightRecord, me);
+ if (inverseSetter) {
+ // Because the rightRecord has a reference back to the leftRecord
+ // we pass on to its setter (if there is one). We've already set
+ // the value on this side so we won't recurse back-and-forth.
+ rightRecord[inverseSetter](leftRecord);
+ }
+ }
+ } else {
+ // The value we received could just be the id of the rightRecord so we just
+ // need to set the FK accordingly and cleanup any cached references.
+ if (!foreignKey) {
+ Ext.raise('No foreignKey specified for "' + me.association.left.role + '" by ' + leftRecord.$className);
+ }
+ modified = (leftRecord.changingKey && !inverse.isMany) || leftRecord.set(foreignKey, rightRecord);
+ // set returns the modifiedFieldNames[] or null if nothing was change
+ if (modified && current && current.isEntity && !current.isEqual(current.getId(), rightRecord)) {
+ // If we just modified the FK value and it no longer matches the id of the
+ // record we had cached (ret), remove references from *both* sides:
+ leftRecord[instanceName] = undefined;
+ if (!inverse.isMany) {
+ current[inverse.getInstanceName()] = undefined;
+ }
+ }
+ }
+ if (options) {
+ if (Ext.isFunction(options)) {
+ options = {
+ callback: options,
+ scope: scope || leftRecord
+ };
+ }
+ return leftRecord.save(options);
+ }
+ }
+});
+
+/**
+ * **This class is never created directly. It should be constructed through associations
+ * in `Ext.data.Model`.**
+ *
+ * Associations enable you to express relationships between different {@link Ext.data.Model Models}.
+ * Consider an ecommerce system where Users can place Orders - there is a one to many relationship
+ * between these Models, one user can have many orders (including 0 orders). Here is what a sample
+ * implementation of this association could look like. This example will be referred to in the
+ * following sections.
+ *
+ * Ext.define('User', {
+ * extend: 'Ext.data.Model',
+ * fields: [{
+ * name: 'id',
+ * type: 'int'
+ * }, 'name']
+ * });
+ *
+ * Ext.define('Order', {
+ * extend: 'Ext.data.Model',
+ * fields: [{
+ * name: 'id',
+ * type: 'int'
+ * }, {
+ * name: 'userId',
+ * type: 'int',
+ * reference: 'User'
+ * }]
+ * });
+ *
+ * # Association Types
+ *
+ * Assocations can describe relationships in 3 ways:
+ *
+ * ## Many To One
+ *
+ * A single entity (`A`) has a relationship with many (`B`) entities. An example of this is
+ * an ecommerce system `User` can have many `Order` entities.
+ *
+ * This can be defined using `Ext.data.schema.ManyToOne` for keyed associations, or
+ * `Ext.data.schema.HasMany` for keyless associations.
+ *
+ * ## One To One
+ *
+ * A less common form of Many To One, a single entity (`A`) has a relationship with at most 1 entity
+ * (`B`). This is often used when partitioning data. For example a `User` may have a single
+ * `UserInfo` object that stores extra metadata about the user.
+ *
+ * This can be defined using `Ext.data.schema.OneToOne` for keyed associations, or
+ * `Ext.data.schema.HasOne` for keyless associations.
+ *
+ * ## Many To Many
+ *
+ * An entity (`A`) may have a have a relationship with many (`B`) entities. That (`B`) entity may
+ * also have a relationship with many `A` entities. For example a single `Student` can have many
+ * `Subject` entities and a single `Subject` can have many `Student` entities.
+ *
+ * This can be defined using `Ext.data.schema.ManyToMany`. Many To Many relationships are read-only
+ * unless used with a `Ext.data.Session`.
+ *
+ *
+ * # Keyed vs Keyless Associations
+ *
+ * Associations can be declared in 2 ways, which are outlined below.
+ *
+ * ## Keyed associations
+ *
+ * A keyed association relies on a field in the model matching the id of another model. Membership
+ * is driven by the key. This is the type of relationship that is typically used in a relational
+ * database. This is declared using the ||reference|| configuration on a model field. An example
+ * of this can be seen above for `User/Order`.
+ *
+ * # Keyless associations
+ *
+ * A keyless association relies on data hierarchy to determine membership. Items are members because
+ * they are contained by another entity. This type of relationship is common with NoSQL databases.
+ * formats. A simple example definition using `User/Order`:
+ *
+ * Ext.define('User', {
+ * extend: 'Ext.data.Model',
+ * fields: [{
+ * name: 'id',
+ * type: 'int'
+ * }, 'name'],
+ * hasMany: 'Order'
+ * });
+ *
+ * Ext.define('Order', {
+ * extend: 'Ext.data.Model',
+ * fields: [{
+ * name: 'id',
+ * type: 'int'
+ * }]
+ * });
+ *
+ * # Advantages of Associations
+ *
+ * Assocations make it easier to work with Models that share a connection. Some of the main
+ * functionality includes:
+ *
+ * ## Generated Accessors/Setters
+ *
+ * Associated models will automatically generate named methods that allow for accessing the
+ * associated data. The names for these are created using a {@link Ext.data.schema.Schema Schema},
+ * to provide a consistent and predictable naming structure.
+ *
+ * Using the example code above, there will be 3 generated methods:
+ * + `User` will have an `orders()` function that returns a `Ext.data.Store` of`Orders`.
+ * + `Order` will have a `getUser` method which will return a `User` Model.
+ * + `Order` will have a `setUser` method that will accept a `User` model or a key value.
+ *
+ * ## Nested Loading
+ *
+ * Nested loading is the ability to load hierarchical associated data from a remote source within
+ * a single request. In the following example, each `User` in the `users` store has an `orders`
+ * store. Each `orders` store is populated with `Order` models read from the request. Each `Order`
+ * model also has a reference back to the appropriate `User`.
+ *
+ * // Sample JSON data returned by /Users
+ * [{
+ * "id": 1,
+ * "name": "User Foo",
+ * "orders": [{
+ * "id": 101,
+ * "userId": 1
+ * }, {
+ * "id": 102,
+ * "userId": 1
+ * }, {
+ * "id": 103,
+ * "userId": 1
+ * }]
+ * }, {
+ * "id": 2,
+ * "name": "User Bar",
+ * "orders": [{
+ * "id": 201,
+ * "userId": 2
+ * }, {
+ * "id": 202,
+ * "userId": 2
+ * }]
+ * }]
+ *
+ * // Application code
+ * var users = new Ext.data.Store({
+ * model: 'User',
+ * proxy: {
+ * type: 'ajax',
+ * url: '/Users'
+ * }
+ * });
+ * users.load(function() {
+ * var user1 = users.first(),
+ * user2 = users.last(),
+ * orders1 = user1.orders(),
+ * orders2 = user2.orders();
+ *
+ * // 3 orders, same reference back to user1
+ * console.log(orders1.getCount(), orders1.first().getUser() === user1);
+ * // 2 orders, same reference back to user2
+ * console.log(orders2.getCount(), orders2.first().getUser() === user2);
+ * });
+ *
+ * ## Binding
+ *
+ * Data binding using {@link Ext.app.ViewModel ViewModels} have functionality to be able
+ * to recognize associated data as part of a bind statement. For example:
+ * + `{user.orders}` binds to the orders store for a user.
+ * + `{order.user.name}` binds to the name of the user taken from the order.
+ *
+ *
+ * # Association Concepts
+ *
+ * ## Roles
+ *
+ * The role is used to determine generated names for an association. By default, the role is
+ * generated from either the field name (in a keyed association) or the model name. This naming
+ * follows a pattern defined by the `Ext.data.schema.Namer`. To change a specific instance,
+ * an explicit role can be specified:
+ *
+ * Ext.define('Thread', {
+ * extend: 'Ext.data.Model',
+ * fields: ['id', 'title']
+ * });
+ *
+ * Ext.define('Post', {
+ * extend: 'Ext.data.Model',
+ * fields: ['id', 'content', {
+ * name: 'threadId',
+ * reference: {
+ * type: 'Thread',
+ * role: 'discussion',
+ * inverse: 'comments'
+ *
+ * }
+ * }]
+ * });
+ *
+ * In the above example, the `Thread` will be decorated with a `comments` method that returns
+ * the store. The `Post` will be decorated with `getDiscussion/setDiscussion` methods.
+ *
+ * ## Generated Methods
+ *
+ * Associations generate methods to allow reading and manipulation on associated data.
+ *
+ * On records that have a "to many" relationship, a single methods that returns a `Ext.data.Store`
+ * is created. See {@link #storeGetter}. On records that have a "to one" relationship, 2 methods
+ * are generated, a {@link #recordGetter getter} and a {@link #recordSetter setter}.
+ *
+ * ## Reflexive
+ *
+ * Associations are reflexive. By declaring one "side" of the relationship, the other is
+ * automatically setup. In the example below, there is no code in the `Thread` entity regarding
+ * the association, however by virtue of the declaration in post, `Thread` is decorated with the
+ * appropriate infrastructure to participate in the association.
+ *
+ * Ext.define('Thread', {
+ * extend: 'Ext.data.Model',
+ * fields: ['id', 'title']
+ * });
+ *
+ * Ext.define('Post', {
+ * extend: 'Ext.data.Model',
+ * fields: ['id', 'content', {
+ * name: 'threadId',
+ * reference: 'Thread'
+ * }]
+ * });
+ *
+ * ## Naming
+ *
+ * Referring to model names in associations depends on their {@link Ext.data.Model#entityName}. See
+ * the "Relative Naming" section in the `Ext.data.schema.Schema` documentation.
+ */
+Ext.define('Ext.data.schema.Association', {
+ isOneToOne: false,
+ isManyToOne: false,
+ isManyToMany: false,
+ /**
+ * @cfg {String} associationKey
+ * The name of the property in the data to read the association from. Defaults to the
+ * name of the associated model.
+ */
+ /**
+ * @method storeGetter
+ * **This is not a real method, it is placeholder documentation for a generated method on
+ * a `Ext.data.Model`.**
+ *
+ * Gets a store configured with the model of the "many" record.
+ * @param {Object/Function} [options] The options for the getter, or a callback function
+ * to execute. If specified as a function, it will act as the `callback` option.
+ *
+ * @param {Boolean} [options.reload] `true` to force the store to reload from the server.
+ *
+ * @param {Object} [options.scope] The `this` reference for the callback.
+ * Defaults to the record.
+ *
+ * @param {Function} [options.success] A function to execute when the store loads successfully.
+ * If the store has already loaded, this will be called immediately and the `Operation` will be
+ * `null`. The success is passed the following parameters:
+ * @param {Ext.data.Store} [options.success.store] The store.
+ * @param {Ext.data.operation.Operation} [options.success.operation] The operation. `null`
+ * if no load occurred.
+ *
+ * @param {Function} [options.failure] A function to execute when the store load fails.
+ * If the store has already loaded, this will not be called.
+ * The failure is passed the following parameters:
+ * @param {Ext.data.Store} [options.failure.store] The store.
+ * @param {Ext.data.operation.Operation} [options.failure.operation] The operation
+ *
+ * @param {Function} [options.callback] A function to execute when the store loads, whether
+ * it is successful or failed. If the store has already loaded, this will be called immediately
+ * and the `Operation` will be `null`. The callback is passed the following parameters:
+ * @param {Ext.data.Store} [options.callback.store] The store.
+ * @param {Ext.data.operation.Operation} [options.callback.operation] The operation. `null`
+ * if no load occurred.
+ * @param {Boolean} [options.callback.success] `true` if the load was successful. If already
+ * loaded this will always be true.
+ *
+ * @param {Object} [scope] The `this` reference for the callback. Defaults to the record.
+ *
+ * @return {Ext.data.Store} The store.
+ */
+ /**
+ * @method recordGetter
+ * **This is not a real method, it is placeholder documentation for a generated method on
+ * a `Ext.data.Model`.**
+ *
+ * Gets a model of the "one" type.
+ * @param {Object/Function} [options] The options for the getter, or a callback function
+ * to execute. If specified as a function, it will act as the `callback` option.
+ *
+ * @param {Boolean} [options.reload] `true` to force the record to reload from the server.
+ *
+ * @param {Object} [options.scope] The `this` reference for the callback.
+ * Defaults to the record.
+ *
+ * @param {Function} [options.success] A function to execute when the record loads successfully.
+ * If the record has already loaded, this will be called immediately and the `Operation` will be
+ * `null`. The success is passed the following parameters:
+ * @param {Ext.data.Model} [options.success.record] The record.
+ * @param {Ext.data.operation.Operation} [options.success.operation] The operation. `null`
+ * if no load occurred.
+ *
+ * @param {Function} [options.failure] A function to execute when the record load fails.
+ * If the record has already loaded, this will not be called.
+ * The failure is passed the following parameters:
+ * @param {Ext.data.Model} [options.failure.record] The record.
+ * @param {Ext.data.operation.Operation} [options.failure.operation] The operation
+ *
+ * @param {Function} [options.callback] A function to execute when the record loads, whether
+ * it is successful or failed. If the record has already loaded, this will be called immediately
+ * and the `Operation` will be `null`. The callback is passed the following parameters:
+ * @param {Ext.data.Model} [options.callback.record] The record.
+ * @param {Ext.data.operation.Operation} [options.callback.operation] The operation. `null`
+ * if no load occurred.
+ * @param {Boolean} [options.callback.success] `true` if the load was successful. If already
+ * loaded this will always be true.
+ *
+ * @param {Object} [scope] The `this` reference for the callback. Defaults to the record.
+ * @return {Ext.data.Model} The record. `null` if the reference has been previously specified
+ * as empty.
+ */
+ /**
+ * @method recordSetter **This is not a real method, it is placeholder documentation
+ * for a generated method on a `Ext.data.Model`.**
+ *
+ * Sets a model of the "one" type.
+ * @param {Ext.data.Model/Object} value The value to set. This can be a model instance,
+ * a key value (if a keyed association) or `null` to clear the value.
+ *
+ * @param {Object/Function} [options] Options to handle callback. If specified as
+ * a function, it will act as the `callback` option. If specified as an object, the params
+ * are the same as {@link Ext.data.Model#save}. If options is specified,
+ * {@link Ext.data.Model#save} will be called on this record.
+ */
+ /**
+ * @cfg {String} name
+ * The name of this association.
+ */
+ /**
+ * @property {Object} owner
+ * Points at either `left` or `right` objects if one is the owning party in this
+ * association or is `null` if there is no owner.
+ * @readonly
+ */
+ owner: null,
+ /**
+ * @property {Ext.Class} definedBy
+ * @readonly
+ */
+ /**
+ * @property {Ext.data.field.Field} field
+ * @readonly
+ */
+ field: null,
+ /**
+ * @property {Ext.data.schema.Schema} schema
+ * @readonly
+ */
+ /**
+ * @property {Boolean} nullable
+ * @readonly
+ */
+ /**
+ * @property {Ext.data.schema.Role} left
+ * @readonly
+ */
+ /**
+ * @property {Ext.data.schema.Role} right
+ * @readonly
+ */
+ constructor: function(config) {
+ var me = this,
+ left, right;
+ Ext.apply(me, config);
+ me.left = left = new me.Left(me, me.left);
+ me.right = right = new me.Right(me, me.right);
+ left.inverse = right;
+ right.inverse = left;
+ },
+ hasField: function() {
+ return !!this.field;
+ },
+ getFieldName: function() {
+ var field = this.field;
+ return field ? field.name : '';
+ }
+});
+
+/**
+ * **This class is never created directly. It should be constructed through associations
+ * in `Ext.data.Model`.**
+ *
+ * This is a specialized version of `Ext.data.schema.ManyToOne` that declares a relationship between
+ * a single entity type and a single related entities. The relationship can be declared as a keyed
+ * or keyless relationship.
+ *
+ * // Keyed
+ * Ext.define('User', {
+ * extend: 'Ext.data.Model',
+ * fields: ['id', 'name', {
+ * name: 'userInfoId',
+ * reference: {
+ * type: 'UserInfo',
+ * unique: true
+ * }
+ * }]
+ * });
+ *
+ * Ext.define('UserInfo', {
+ * extend: 'Ext.data.Model',
+ * fields: ['id', 'secretKey']
+ * });
+ *
+ * // Keyless
+ * Ext.define('User', {
+ * extend: 'Ext.data.Model',
+ * fields: ['id', 'name'],
+ * hasOne: 'UserInfo'
+ * });
+ *
+ * Ext.define('Ticket', {
+ * extend: 'Ext.data.Model',
+ * fields: ['id', 'secretKey']
+ * });
+ *
+ * // Generated methods
+ * var user = new User();
+ * user.getUserInfo();
+ * user.setUserInfo();
+ *
+ * var info = new UserInfo();
+ * info.getUser();
+ * info.setUser();
+ *
+ *
+ * var ticket = new Ticket();
+ * ticket.setCustomer(customer);
+ * console.log(ticket.getCustomer()); // The customer object
+ *
+ * By declaring a keyed relationship, extra functionality is gained that maintains
+ * the key field in the model as changes are made to the association.
+ *
+ * For available configuration options, see {@link Ext.data.schema.Reference}.
+ * Each record type will have a {@link Ext.data.schema.Association#recordGetter getter} and
+ * {@link Ext.data.schema.Association#recordSetter setter}.
+ */
+Ext.define('Ext.data.schema.OneToOne', {
+ extend: Ext.data.schema.Association,
+ isOneToOne: true,
+ isToOne: true,
+ kind: 'one-to-one',
+ Left: Ext.define(null, {
+ extend: 'Ext.data.schema.Role',
+ onDrop: function(rightRecord, session) {
+ var leftRecord = this.getAssociatedItem(rightRecord);
+ rightRecord[this.getInstanceName()] = null;
+ if (leftRecord) {
+ leftRecord[this.inverse.getInstanceName()] = null;
+ }
+ },
+ onIdChanged: function(rightRecord, oldId, newId) {
+ var leftRecord = this.getAssociatedItem(rightRecord),
+ fieldName = this.association.getFieldName();
+ if (!rightRecord.session && leftRecord && fieldName) {
+ leftRecord.set(fieldName, newId);
+ }
+ },
+ createGetter: function() {
+ var me = this;
+ return function() {
+ // 'this' refers to the Model instance inside this function
+ return me.doGet(this);
+ };
+ },
+ createSetter: function() {
+ var me = this;
+ return function(value) {
+ // 'this' refers to the Model instance inside this function
+ return me.doSet(this, value);
+ };
+ },
+ doGet: function(rightRecord) {
+ // Consider the Department entity with a managerId to a User entity. The
+ // Department is on the left (the FK holder's side) so we are implementing the
+ // guts of the getManagerDepartment method we place on the User entity. Since
+ // we represent the "managerDepartment" role and as such our goal is to get a
+ // Department instance, we start that from the User (rightRecord). Sadly that
+ // record has no FK back to us.
+ var instanceName = this.getInstanceName(),
+ // ex "managerDepartment"
+ ret = rightRecord[instanceName],
+ session = rightRecord.session;
+ if (!ret && session) {}
+ // @TODO: session - we'll cache the result on the record as always
+ // but to get it we must ask the session
+ return ret || null;
+ },
+ doSet: function(rightRecord, leftRecord) {
+ // We are the guts of the setManagerDepartment method we place on the User
+ // entity. Our goal here is to establish the relationship between the new
+ // Department (leftRecord) and the User (rightRecord).
+ var instanceName = this.getInstanceName(),
+ // ex "managerDepartment"
+ ret = rightRecord[instanceName],
+ inverseSetter = this.inverse.setterName;
+ // setManager for Department
+ if (ret !== leftRecord) {
+ rightRecord[instanceName] = leftRecord;
+ if (inverseSetter) {
+ // Because the FK is owned by the inverse record, we delegate the
+ // majority of work to its setter. We've already locked in the only
+ // thing we keep on this side so we won't recurse back-and-forth.
+ leftRecord[inverseSetter](rightRecord);
+ }
+ rightRecord.onAssociatedRecordSet(leftRecord, this);
+ }
+ return ret;
+ },
+ read: function(rightRecord, node, fromReader, readOptions) {
+ var me = this,
+ leftRecords = me.callParent([
+ rightRecord,
+ node,
+ fromReader,
+ readOptions
+ ]),
+ leftRecord;
+ if (leftRecords) {
+ leftRecord = leftRecords[0];
+ if (leftRecord) {
+ leftRecord[me.inverse.getInstanceName()] = rightRecord;
+ rightRecord[me.getInstanceName()] = leftRecord;
+ // Inline associations should *not* arrive on the "data" object:
+ delete rightRecord.data[me.role];
+ }
+ }
+ }
+ }),
+ Right: Ext.define(null, {
+ extend: 'Ext.data.schema.Role',
+ left: false,
+ side: 'right',
+ createGetter: function() {
+ // As the target of the FK (say "manager" for the Department entity) this
+ // getter is responsible for getting the entity referenced by the FK value.
+ var me = this;
+ return function(options, scope) {
+ // 'this' refers to the Model instance inside this function
+ return me.doGetFK(this, options, scope);
+ };
+ },
+ createSetter: function() {
+ var me = this;
+ return function(value, options, scope) {
+ // 'this' refers to the Model instance inside this function
+ return me.doSetFK(this, value, options, scope);
+ };
+ },
+ onDrop: function(leftRecord, session) {
+ var me = this,
+ field = me.association.field,
+ rightRecord = me.getAssociatedItem(leftRecord),
+ id;
+ if (me.inverse.owner) {
+ if (session && field) {
+ id = leftRecord.get(field.name);
+ if (id || id === 0) {
+ rightRecord = session.getEntry(me.cls, id).record;
+ if (rightRecord) {
+ rightRecord.drop();
+ }
+ }
+ } else {
+ if (rightRecord) {
+ rightRecord.drop();
+ }
+ }
+ }
+ if (field) {
+ leftRecord.set(field.name, null);
+ }
+ leftRecord[me.getInstanceName()] = null;
+ if (rightRecord) {
+ rightRecord[me.inverse.getInstanceName()] = null;
+ }
+ },
+ onValueChange: function(leftRecord, session, newValue) {
+ // Important to get the record before changing the key.
+ var me = this,
+ rightRecord = leftRecord[me.getOldInstanceName()] || me.getAssociatedItem(leftRecord),
+ // eslint-disable-line max-len
+ hasNewValue = newValue || newValue === 0,
+ instanceName = me.getInstanceName(),
+ cls = me.cls;
+ leftRecord.changingKey = true;
+ me.doSetFK(leftRecord, newValue);
+ if (!hasNewValue) {
+ leftRecord[instanceName] = null;
+ } else if (session && cls) {
+ // Setting to undefined is important so that we can load the record later.
+ leftRecord[instanceName] = session.peekRecord(cls, newValue) || undefined;
+ }
+ if (me.inverse.owner && rightRecord) {
+ me.association.schema.queueKeyCheck(rightRecord, me);
+ }
+ leftRecord.changingKey = false;
+ },
+ checkKeyForDrop: function(rightRecord) {
+ var leftRecord = this.inverse.getAssociatedItem(rightRecord);
+ if (!leftRecord) {
+ // Not reassigned to another parent
+ rightRecord.drop();
+ }
+ },
+ read: function(leftRecord, node, fromReader, readOptions) {
+ var me = this,
+ rightRecords = me.callParent([
+ leftRecord,
+ node,
+ fromReader,
+ readOptions
+ ]),
+ rightRecord, field, fieldName, session, refs, id, oldId, setKey, data;
+ if (rightRecords) {
+ rightRecord = rightRecords[0];
+ field = me.association.field;
+ if (field) {
+ fieldName = field.name;
+ }
+ session = leftRecord.session;
+ data = leftRecord.data;
+ if (rightRecord) {
+ if (session) {
+ refs = session.getRefs(rightRecord, this.inverse, true);
+ // If we have an existing reference in the session, or we don't and the data
+ // is undefined, allow the nested load to go ahead
+ setKey = (refs && refs[leftRecord.id]) || (data[fieldName] === undefined);
+ } else {
+ setKey = true;
+ }
+ if (setKey) {
+ // We want to poke the inferred key onto record if it exists, but we don't
+ // want to mess with the dirty or modified state of the record.
+ if (field) {
+ oldId = data[fieldName];
+ id = rightRecord.id;
+ if (oldId !== id) {
+ data[fieldName] = id;
+ if (session) {
+ session.updateReference(leftRecord, field, id, oldId);
+ }
+ }
+ }
+ rightRecord[me.inverse.getInstanceName()] = leftRecord;
+ leftRecord[me.getInstanceName()] = rightRecord;
+ }
+ // Inline associations should *not* arrive on the "data" object:
+ delete data[me.role];
+ }
+ }
+ }
+ })
+});
+
+/**
+ * **This class is never created directly. It should be constructed through associations
+ * in `Ext.data.Model`.**
+ *
+ * Declares a relationship between a single entity type and multiple related entities.
+ * The relationship can be declared as a keyed or keyless relationship.
+ *
+ * // Keyed
+ * Ext.define('Customer', {
+ * extend: 'Ext.data.Model',
+ * fields: ['id', 'name']
+ * });
+ *
+ * Ext.define('Ticket', {
+ * extend: 'Ext.data.Model',
+ * fields: ['id', 'title', {
+ * name: 'customerId',
+ * reference: 'Customer'
+ * }]
+ * });
+ *
+ * // Keyless
+ * Ext.define('Customer', {
+ * extend: 'Ext.data.Model',
+ * fields: ['id', 'name'],
+ * hasMany: 'Ticket'
+ * });
+ *
+ * Ext.define('Ticket', {
+ * extend: 'Ext.data.Model',
+ * fields: ['id', 'title']
+ * });
+ *
+ * // Generated methods
+ * var customer = new Customer();
+ * customer.tickets();
+ *
+ * var ticket = new Ticket();
+ * ticket.getCustomer();
+ * ticket.setCustomer();
+ *
+ * By declaring a keyed relationship, extra functionality is gained that maintains
+ * the key field in the model as changes are made to the association.
+ *
+ * For available configuration options, see {@link Ext.data.schema.Reference}.
+ * The "one" record type will have a generated {@link Ext.data.schema.Association#storeGetter}.
+ * The "many" record type will have a {@link Ext.data.schema.Association#recordGetter getter}
+ * and {@link Ext.data.schema.Association#recordSetter setter}.
+ */
+Ext.define('Ext.data.schema.ManyToOne', {
+ extend: Ext.data.schema.Association,
+ isManyToOne: true,
+ isToOne: true,
+ kind: 'many-to-one',
+ Left: Ext.define(null, {
+ extend: 'Ext.data.schema.Role',
+ isMany: true,
+ onDrop: function(rightRecord, session) {
+ var me = this,
+ store = me.getAssociatedItem(rightRecord),
+ leftRecords, len, i, id;
+ if (store) {
+ // Removing will cause the foreign key to be set to null.
+ leftRecords = store.removeAll();
+ if (leftRecords && me.inverse.owner) {
+ // If we're a child, we need to destroy all the "tickets"
+ for (i = 0 , len = leftRecords.length; i < len; ++i) {
+ leftRecords[i].drop();
+ }
+ }
+ store.destroy();
+ rightRecord[me.getStoreName()] = null;
+ } else if (session) {
+ leftRecords = session.getRefs(rightRecord, me);
+ if (leftRecords) {
+ for (id in leftRecords) {
+ leftRecords[id].drop();
+ }
+ }
+ }
+ },
+ onIdChanged: function(rightRecord, oldId, newId) {
+ var fieldName = this.association.getFieldName(),
+ store = this.getAssociatedItem(rightRecord),
+ leftRecords, i, len, filter;
+ if (store) {
+ filter = store.getFilters().get(this.$roleFilterId);
+ if (filter) {
+ filter.setValue(newId);
+ }
+ // A session will automatically handle this updating. If we don't have a field
+ // then there's nothing to do here.
+ if (!rightRecord.session && fieldName) {
+ leftRecords = store.getDataSource().items;
+ for (i = 0 , len = leftRecords.length; i < len; ++i) {
+ leftRecords[i].set(fieldName, newId);
+ }
+ }
+ }
+ },
+ processUpdate: function(session, associationData) {
+ var me = this,
+ entityType = me.inverse.cls,
+ items = associationData.R,
+ id, rightRecord, store, leftRecords;
+ if (items) {
+ for (id in items) {
+ rightRecord = session.peekRecord(entityType, id);
+ if (rightRecord) {
+ leftRecords = session.getEntityList(me.cls, items[id]);
+ store = me.getAssociatedItem(rightRecord);
+ if (store) {
+ store.loadData(leftRecords);
+ store.complete = true;
+ } else {
+ // We don't have a store. Create it and add the records.
+ rightRecord[me.getterName](null, null, leftRecords);
+ }
+ } else {
+ session.onInvalidAssociationEntity(entityType, id);
+ }
+ }
+ }
+ },
+ findRecords: function(session, rightRecord, leftRecords, allowInfer) {
+ var ret = leftRecords,
+ refs = session.getRefs(rightRecord, this, true),
+ field = this.association.field,
+ fieldName, leftRecord, id, i, len, seen;
+ if (field && (refs || allowInfer)) {
+ fieldName = field.name;
+ ret = [];
+ if (leftRecords) {
+ seen = {};
+ // Loop over the records returned by the server and
+ // check they all still belong. If the session doesn't have any prior knowledge
+ // and we're allowed to infer the parent id (via nested loading), only do so if
+ // we explicitly have an id specified
+ for (i = 0 , len = leftRecords.length; i < len; ++i) {
+ leftRecord = leftRecords[i];
+ id = leftRecord.id;
+ if (refs && refs[id]) {
+ ret.push(leftRecord);
+ } else if (allowInfer && leftRecord.data[fieldName] === undefined) {
+ ret.push(leftRecord);
+ leftRecord.data[fieldName] = rightRecord.id;
+ session.updateReference(leftRecord, field, rightRecord.id, undefined);
+ }
+ seen[id] = true;
+ }
+ }
+ // Loop over the expected set and include any missing records.
+ if (refs) {
+ for (id in refs) {
+ if (!seen || !seen[id]) {
+ ret.push(refs[id]);
+ }
+ }
+ }
+ }
+ return ret;
+ },
+ processLoad: function(store, rightRecord, leftRecords, session) {
+ var ret = leftRecords;
+ if (session) {
+ // Allow infer here, we only get called when loading an associated store
+ ret = this.findRecords(session, rightRecord, leftRecords, true);
+ }
+ this.onLoadMany(rightRecord, ret, session);
+ return ret;
+ },
+ adoptAssociated: function(rightRecord, session) {
+ var store = this.getAssociatedItem(rightRecord),
+ leftRecords, i, len;
+ if (store) {
+ store.setSession(session);
+ leftRecords = store.getData().items;
+ for (i = 0 , len = leftRecords.length; i < len; ++i) {
+ session.adopt(leftRecords[i]);
+ }
+ }
+ },
+ createGetter: function() {
+ var me = this;
+ return function(options, scope, leftRecords) {
+ // 'this' refers to the Model instance inside this function
+ return me.getAssociatedStore(this, options, scope, leftRecords, true);
+ };
+ },
+ createSetter: null,
+ // no setter for an isMany side
+ onAddToMany: function(store, leftRecords) {
+ var rightRecord = store.getAssociatedEntity();
+ if (this.association.field) {
+ this.syncFK(leftRecords, rightRecord, false);
+ } else {
+ this.setInstances(rightRecord, leftRecords);
+ }
+ },
+ onLoadMany: function(rightRecord, leftRecords, session) {
+ this.setInstances(rightRecord, leftRecords, session);
+ },
+ onRemoveFromMany: function(store, leftRecords) {
+ if (this.association.field) {
+ this.syncFK(leftRecords, store.getAssociatedEntity(), true);
+ } else {
+ this.setInstances(null, leftRecords);
+ }
+ },
+ read: function(rightRecord, node, fromReader, readOptions) {
+ var me = this,
+ // We use the inverse role here since we're setting ourselves
+ // on the other record
+ instanceName = me.inverse.getInstanceName(),
+ leftRecords = me.callParent([
+ rightRecord,
+ node,
+ fromReader,
+ readOptions
+ ]),
+ store, len, i;
+ if (leftRecords) {
+ // Create the store and dump the data
+ store = rightRecord[me.getterName](null, null, leftRecords);
+ // Inline associations should *not* arrive on the "data" object:
+ delete rightRecord.data[me.role];
+ leftRecords = store.getData().items;
+ for (i = 0 , len = leftRecords.length; i < len; ++i) {
+ leftRecords[i][instanceName] = rightRecord;
+ }
+ }
+ },
+ setInstances: function(rightRecord, leftRecords, session) {
+ var instanceName = this.inverse.getInstanceName(),
+ id = rightRecord ? rightRecord.getId() : null,
+ field = this.association.field,
+ len = leftRecords.length,
+ i, leftRecord, oldId, data, name;
+ for (i = 0; i < len; ++i) {
+ leftRecord = leftRecords[i];
+ leftRecord[instanceName] = rightRecord;
+ if (field) {
+ name = field.name;
+ data = leftRecord.data;
+ oldId = data[name];
+ if (oldId !== id) {
+ data[name] = id;
+ if (session) {
+ session.updateReference(leftRecord, field, id, oldId);
+ }
+ }
+ }
+ }
+ },
+ syncFK: function(leftRecords, rightRecord, clearing) {
+ // We are called to set things like the FK (ticketId) of an array of Comment
+ // entities. The best way to do that is call the setter on the Comment to set
+ // the Ticket. Since we are setting the Ticket, the name of that setter is on
+ // our inverse role.
+ var foreignKeyName = this.association.getFieldName(),
+ inverse = this.inverse,
+ setter = inverse.setterName,
+ // setTicket
+ instanceName = inverse.getInstanceName(),
+ i = leftRecords.length,
+ id = rightRecord.getId(),
+ different, leftRecord, val;
+ while (i-- > 0) {
+ leftRecord = leftRecords[i];
+ different = !leftRecord.isEqual(id, leftRecord.get(foreignKeyName));
+ val = clearing ? null : rightRecord;
+ if (different !== clearing) {
+ // clearing === true
+ // different === true :: leave alone (not associated anymore)
+ // ** different === false :: null the value (no longer associated)
+ //
+ // clearing === false
+ // ** different === true :: set the value (now associated)
+ // different === false :: leave alone (already associated)
+ //
+ leftRecord.changingKey = true;
+ leftRecord[setter](val);
+ leftRecord.changingKey = false;
+ } else {
+ // Ensure we set the instance, we may only have the key
+ leftRecord[instanceName] = val;
+ }
+ }
+ }
+ }),
+ Right: Ext.define(null, {
+ extend: 'Ext.data.schema.Role',
+ left: false,
+ side: 'right',
+ onDrop: function(leftRecord, session) {
+ // By virtue of being dropped, this record will be removed
+ // from any stores it belonged to. The only case we have
+ // to worry about is if we have a session but were not yet
+ // part of any stores, so we need to clear the foreign key.
+ var field = this.association.field;
+ if (field) {
+ leftRecord.set(field.name, null);
+ }
+ leftRecord[this.getInstanceName()] = null;
+ },
+ createGetter: function() {
+ // As the target of the FK (say "ticket" for the Comment entity) this
+ // getter is responsible for getting the entity referenced by the FK value.
+ var me = this;
+ return function(options, scope) {
+ // 'this' refers to the Comment instance inside this function
+ return me.doGetFK(this, options, scope);
+ };
+ },
+ createSetter: function() {
+ var me = this;
+ return function(rightRecord, options, scope) {
+ // 'this' refers to the Comment instance inside this function
+ return me.doSetFK(this, rightRecord, options, scope);
+ };
+ },
+ checkMembership: function(session, leftRecord) {
+ var field = this.association.field,
+ store;
+ if (field) {
+ store = this.getSessionStore(session, leftRecord.get(field.name));
+ // Check we're not in the middle of an add to the store.
+ if (store && !store.contains(leftRecord)) {
+ store.add(leftRecord);
+ }
+ }
+ },
+ onValueChange: function(leftRecord, session, newValue, oldValue) {
+ // If we have a session, we may be able to find the new store this belongs to
+ // If not, the best we can do is to remove the record from the associated store/s.
+ var me = this,
+ instanceName = me.getInstanceName(),
+ cls = me.cls,
+ hasNewValue, joined, store, i, associated, rightRecord;
+ if (!leftRecord.changingKey) {
+ hasNewValue = newValue || newValue === 0;
+ if (!hasNewValue) {
+ leftRecord[instanceName] = null;
+ }
+ if (session) {
+ // Find the store that holds this record and remove it if possible.
+ store = me.getSessionStore(session, oldValue);
+ if (store) {
+ store.remove(leftRecord);
+ }
+ // If we have a new value, try and find it and push it into the new store.
+ if (hasNewValue) {
+ store = me.getSessionStore(session, newValue);
+ if (store && !store.isLoading()) {
+ store.add(leftRecord);
+ }
+ if (cls) {
+ rightRecord = session.peekRecord(cls, newValue);
+ }
+ // Setting to undefined is important so that we can load the record later.
+ leftRecord[instanceName] = rightRecord || undefined;
+ }
+ } else {
+ joined = leftRecord.joined;
+ if (joined) {
+ // Loop backwards because the store remove may cause unjoining, which means
+ // removal from the joined array.
+ for (i = joined.length - 1; i >= 0; i--) {
+ store = joined[i];
+ if (store.isStore) {
+ associated = store.getAssociatedEntity();
+ if (associated && associated.self === me.cls && associated.getId() === oldValue) {
+ store.remove(leftRecord);
+ }
+ }
+ }
+ }
+ }
+ }
+ if (me.owner && newValue === null) {
+ me.association.schema.queueKeyCheck(leftRecord, me);
+ }
+ },
+ checkKeyForDrop: function(leftRecord) {
+ var field = this.association.field;
+ if (leftRecord.get(field.name) === null) {
+ leftRecord.drop();
+ }
+ },
+ getSessionStore: function(session, value) {
+ // May not have the cls loaded yet
+ var cls = this.cls,
+ rec;
+ if (cls) {
+ rec = session.peekRecord(cls, value);
+ if (rec) {
+ return this.inverse.getAssociatedItem(rec);
+ }
+ }
+ },
+ read: function(leftRecord, node, fromReader, readOptions) {
+ var rightRecords = this.callParent([
+ leftRecord,
+ node,
+ fromReader,
+ readOptions
+ ]),
+ rightRecord;
+ if (rightRecords) {
+ rightRecord = rightRecords[0];
+ if (rightRecord) {
+ leftRecord[this.getInstanceName()] = rightRecord;
+ delete leftRecord.data[this.role];
+ }
+ }
+ }
+ })
+});
+
+/**
+ * This relationship describes the case where any one entity of one type may relate to any
+ * number of entities of another type, and also in the reverse.
+ *
+ * This form of association cannot store id's in the related entities since that would
+ * limit the number of related entities to one for the entity with the foreign key. Instead,
+ * these relationships are typically implemented using a so-called "matrix" table. This
+ * table typically has two columns to hold the id's of a pair of related entities. This
+ * pair of id's is unique in the matrix table.
+ *
+ * # Declaration Forms
+ *
+ * // Fully spelled out - all properties are their defaults:
+ *
+ * Ext.define('App.models.Group', {
+ * extend: 'Ext.data.Model',
+ *
+ * manyToMany: {
+ * UserGroups: {
+ * type: 'User',
+ * role: 'users',
+ * field: 'userId',
+ * right: {
+ * field: 'groupId',
+ * role: 'groups'
+ * }
+ * }
+ * }
+ * });
+ *
+ * // Eliminate "right" object and use boolean to indicate Group is on the
+ * // right. By default, left/right is determined by alphabetic order.
+ *
+ * Ext.define('App.models.Group', {
+ * extend: 'Ext.data.Model',
+ *
+ * manyToMany: {
+ * UserGroups: {
+ * type: 'User',
+ * role: 'users',
+ * field: 'userId',
+ * right: true
+ * }
+ * }
+ * });
+ *
+ * // Eliminate object completely and rely on string to name the other type. Still
+ * // keep Group on the "right".
+ *
+ * Ext.define('App.models.Group', {
+ * extend: 'Ext.data.Model',
+ *
+ * manyToMany: {
+ * UserGroups: 'User#' // '#' is on the side (left or right) of Group
+ * }
+ * });
+ *
+ * // Remove explicit matrix name and keep Group on the "right". Generated matrixName
+ * // remains "UserGroups".
+ *
+ * Ext.define('App.models.Group', {
+ * extend: 'Ext.data.Model',
+ *
+ * manyToMany: [
+ * 'User#'
+ * ]
+ * });
+ *
+ * // Minimal definition but now Group is on the "left" since "Group" sorts before
+ * // "User". Generated matrixName is now "GroupUsers".
+ *
+ * Ext.define('App.models.Group', {
+ * extend: 'Ext.data.Model',
+ *
+ * manyToMany: [
+ * 'User'
+ * ]
+ * });
+ */
+Ext.define('Ext.data.schema.ManyToMany', {
+ extend: Ext.data.schema.Association,
+ isManyToMany: true,
+ isToMany: true,
+ kind: 'many-to-many',
+ Left: Ext.define(null, {
+ extend: 'Ext.data.schema.Role',
+ isMany: true,
+ digitRe: /^\d+$/,
+ findRecords: function(session, rightRecord, leftRecords) {
+ var slice = session.getMatrixSlice(this.inverse, rightRecord.id),
+ members = slice.members,
+ ret = [],
+ cls = this.cls,
+ seen, i, len, id, member, leftRecord;
+ if (leftRecords) {
+ seen = {};
+ // Loop over the records returned by the server and
+ // check they all still belong
+ for (i = 0 , len = leftRecords.length; i < len; ++i) {
+ leftRecord = leftRecords[i];
+ id = leftRecord.id;
+ member = members[id];
+ if (!(member && member[2] === -1)) {
+ ret.push(leftRecord);
+ }
+ seen[id] = true;
+ }
+ }
+ // Loop over the expected set and include any missing records.
+ for (id in members) {
+ member = members[id];
+ if (!seen || !seen[id] && (member && member[2] !== -1)) {
+ leftRecord = session.peekRecord(cls, id);
+ if (leftRecord) {
+ ret.push(leftRecord);
+ }
+ }
+ }
+ return ret;
+ },
+ onIdChanged: function(rightRecord, oldId, newId) {
+ var store = this.getAssociatedItem(rightRecord);
+ if (store) {
+ store.getFilters().get(this.$roleFilterId).setValue(newId);
+ }
+ },
+ processLoad: function(store, rightRecord, leftRecords, session) {
+ var ret = leftRecords;
+ if (session) {
+ ret = this.findRecords(session, rightRecord, leftRecords);
+ this.onAddToMany(store, ret, true);
+ }
+ return ret;
+ },
+ processUpdate: function(session, associationData) {
+ var me = this,
+ entityType = me.inverse.cls,
+ items = associationData.R,
+ id, rightRecord, store, leftRecords;
+ if (items) {
+ for (id in items) {
+ rightRecord = session.peekRecord(entityType, id);
+ if (rightRecord) {
+ leftRecords = session.getEntityList(me.cls, items[id]);
+ store = me.getAssociatedItem(rightRecord);
+ if (store) {
+ store.loadData(leftRecords);
+ store.complete = true;
+ } else {
+ // We don't have a store. Create it and add the records.
+ rightRecord[me.getterName](null, null, leftRecords);
+ }
+ } else {
+ session.onInvalidAssociationEntity(entityType, id);
+ }
+ }
+ }
+ me.processMatrixBlock(session, associationData.C, 1);
+ me.processMatrixBlock(session, associationData.D, -1);
+ },
+ checkMembership: function(session, rightRecord) {
+ var matrix = session.getMatrix(this.association, true),
+ side, entityType, inverse, slice, slices, id, members, member, leftRecord, store;
+ if (!matrix) {
+ return;
+ }
+ side = this.left ? matrix.right : matrix.left;
+ entityType = side.inverse.role.cls;
+ inverse = this.inverse;
+ slices = side.slices;
+ if (slices) {
+ slice = slices[rightRecord.id];
+ if (slice) {
+ members = slice.members;
+ for (id in members) {
+ member = members[id];
+ if (member[2] !== -1) {
+ // Do we have the record in the session?
+ // If so, do we also have the store?
+ leftRecord = session.peekRecord(entityType, id);
+ if (leftRecord) {
+ store = inverse.getAssociatedItem(leftRecord);
+ if (store) {
+ store.matrixUpdate = 1;
+ store.add(rightRecord);
+ store.matrixUpdate = 0;
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ onStoreCreate: function(store, session, id) {
+ var me = this,
+ matrix;
+ if (session) {
+ // If we are creating a store of say Groups in a UserGroups matrix, we want
+ // to traverse the inverse side of the matrix (Users) because the id we have
+ // is that of the User to which these Groups are associated.
+ matrix = session.getMatrixSlice(me.inverse, id);
+ matrix.attach(store);
+ matrix.notify = me.onMatrixUpdate;
+ matrix.scope = me;
+ }
+ },
+ processMatrixBlock: function(session, leftKeys, state) {
+ var inverse = this.inverse,
+ digitRe = this.digitRe,
+ slice, id;
+ if (leftKeys) {
+ for (id in leftKeys) {
+ // We may not have the record available to pull out the id, so the best we can
+ // do here is try to detect a number id.
+ if (digitRe.test(id)) {
+ id = parseInt(id, 10);
+ }
+ slice = session.getMatrixSlice(inverse, id);
+ slice.update(leftKeys[id], state);
+ }
+ }
+ },
+ createGetter: function() {
+ var me = this;
+ return function(options, scope, leftRecords) {
+ // 'this' refers to the Model instance inside this function
+ return me.getAssociatedStore(this, options, scope, leftRecords, false);
+ };
+ },
+ /*
+ * This method is called when records are added to the association store. If this
+ * is happening as a side-effect of the underlying matrix update, we skip telling
+ * the matrix what it already knows. Otherwise we need to tell the matrix of the
+ * changes on this side so that they can be reflected on the other side.
+ */
+ onAddToMany: function(store, leftRecords, load) {
+ if (!store.matrixUpdate) {
+ store.matrixUpdate = 1;
+ // By default the "load" param is really the index, but we call this manually
+ // in a few spots to indicate it's a default load
+ store.matrix.update(leftRecords, load === true ? 0 : 1);
+ store.matrixUpdate = 0;
+ }
+ },
+ /*
+ * This method is called when records are removed from the association store. The
+ * same logic applies here as in onAddToMany with respect to the update that may
+ * or may not be taking place on the underlying matrix.
+ */
+ onRemoveFromMany: function(store, records) {
+ if (!store.matrixUpdate) {
+ store.matrixUpdate = 1;
+ store.matrix.update(records, -1);
+ store.matrixUpdate = 0;
+ }
+ },
+ read: function(rightRecord, node, fromReader, readOptions) {
+ var me = this,
+ leftRecords = me.callParent([
+ rightRecord,
+ node,
+ fromReader,
+ readOptions
+ ]);
+ if (leftRecords) {
+ // Create the store and dump the data
+ rightRecord[me.getterName](null, null, leftRecords);
+ // Inline associations should *not* arrive on the "data" object:
+ delete rightRecord.data[me.role];
+ }
+ },
+ onMatrixUpdate: function(matrixSlice, id, state) {
+ var store = matrixSlice.store,
+ index, leftRecord, entry;
+ if (store && !store.loading && !store.matrixUpdate) {
+ store.matrixUpdate = 1;
+ index = store.indexOfId(id);
+ if (state < 0) {
+ if (index >= 0) {
+ store.remove([
+ index
+ ]);
+ }
+ } else if (index < 0) {
+ entry = store.getSession().getEntry(this.type, id);
+ leftRecord = entry && entry.record;
+ if (leftRecord) {
+ store.add(leftRecord);
+ }
+ }
+ store.matrixUpdate = 0;
+ }
+ },
+ adoptAssociated: function(record, session) {
+ var store = this.getAssociatedItem(record),
+ records, i, len;
+ if (store) {
+ store.setSession(session);
+ this.onStoreCreate(store, session, record.getId());
+ records = store.getData().items;
+ for (i = 0 , len = records.length; i < len; ++i) {
+ session.adopt(records[i]);
+ }
+ }
+ }
+ }, function() {
+ var Left = this;
+ // Left is created but ManyToMany may not yet be created
+ Ext.ClassManager.onCreated(function() {
+ Ext.data.schema.ManyToMany.prototype.Right = Ext.define(null, {
+ extend: Left,
+ left: false,
+ side: 'right'
+ });
+ }, null, 'Ext.data.schema.ManyToMany');
+ })
+});
+
+/**
+ * General purpose inflector class that {@link #pluralize pluralizes},
+ * {@link #singularize singularizes} and {@link #ordinalize ordinalizes} words. Sample usage:
+ *
+ * //turning singular words into plurals
+ * Ext.util.Inflector.pluralize('word'); //'words'
+ * Ext.util.Inflector.pluralize('person'); //'people'
+ * Ext.util.Inflector.pluralize('sheep'); //'sheep'
+ *
+ * //turning plurals into singulars
+ * Ext.util.Inflector.singularize('words'); //'word'
+ * Ext.util.Inflector.singularize('people'); //'person'
+ * Ext.util.Inflector.singularize('sheep'); //'sheep'
+ *
+ * //ordinalizing numbers
+ * Ext.util.Inflector.ordinalize(11); //"11th"
+ * Ext.util.Inflector.ordinalize(21); //"21st"
+ * Ext.util.Inflector.ordinalize(1043); //"1043rd"
+ *
+ * # Customization
+ *
+ * The Inflector comes with a default set of US English pluralization rules. These can be augmented
+ * with additional rules if the default rules do not meet your application's requirements,
+ * or swapped out entirely for other languages. Here is how we might add a rule that pluralizes
+ * "ox" to "oxen":
+ *
+ * Ext.util.Inflector.plural(/^(ox)$/i, "$1en");
+ *
+ * Each rule consists of two items - a regular expression that matches one or more rules,
+ * and a replacement string. In this case, the regular expression will only match the string "ox",
+ * and will replace that match with "oxen". Here's how we could add the inverse rule:
+ *
+ * Ext.util.Inflector.singular(/^(ox)en$/i, "$1");
+ *
+ * Note that the ox/oxen rules are present by default.
+ */
+Ext.define('Ext.util.Inflector', {
+ /* Begin Definitions */
+ singleton: true,
+ /* End Definitions */
+ /* eslint-disable no-multi-spaces */
+ /**
+ * @private
+ * The registered plural tuples. Each item in the array should contain two items - the first
+ * must be a regular expression that matchers the singular form of a word, the second must be
+ * a String that replaces the matched part of the regular expression. This is managed by the
+ * {@link #plural} method.
+ * @property {Array} plurals
+ */
+ plurals: [
+ [
+ (/(quiz)$/i),
+ "$1zes"
+ ],
+ [
+ (/^(ox)$/i),
+ "$1en"
+ ],
+ [
+ (/([m|l])ouse$/i),
+ "$1ice"
+ ],
+ [
+ (/(matr|vert|ind)ix|ex$/i),
+ "$1ices"
+ ],
+ [
+ (/(x|ch|ss|sh)$/i),
+ "$1es"
+ ],
+ [
+ (/([^aeiouy]|qu)y$/i),
+ "$1ies"
+ ],
+ [
+ (/(hive)$/i),
+ "$1s"
+ ],
+ [
+ (/(?:([^f])fe|([lr])f)$/i),
+ "$1$2ves"
+ ],
+ [
+ (/sis$/i),
+ "ses"
+ ],
+ [
+ (/([ti])um$/i),
+ "$1a"
+ ],
+ [
+ (/(buffal|tomat|potat)o$/i),
+ "$1oes"
+ ],
+ [
+ (/(bu)s$/i),
+ "$1ses"
+ ],
+ [
+ (/(alias|status|sex)$/i),
+ "$1es"
+ ],
+ [
+ (/(octop|vir)us$/i),
+ "$1i"
+ ],
+ [
+ (/(ax|test)is$/i),
+ "$1es"
+ ],
+ [
+ (/^(p)erson$/i),
+ "$1eople"
+ ],
+ [
+ (/^(m)an$/i),
+ "$1en"
+ ],
+ [
+ (/(.*)(child)(ren)?$/i),
+ "$1$2ren"
+ ],
+ [
+ (/s$/i),
+ "s"
+ ],
+ [
+ (/$/),
+ "s"
+ ]
+ ],
+ /**
+ * @private
+ * The set of registered singular matchers. Each item in the array should contain two items -
+ * the first must be a regular expression that matches the plural form of a word, the second
+ * must be a String that replaces the matched part of the regular expression. This is managed
+ * by the {@link #singular} method.
+ * @property {Array} singulars
+ */
+ singulars: [
+ [
+ (/(address)$/i),
+ "$1"
+ ],
+ [
+ (/(quiz)zes$/i),
+ "$1"
+ ],
+ [
+ (/(matr)ices$/i),
+ "$1ix"
+ ],
+ [
+ (/(vert|ind)ices$/i),
+ "$1ex"
+ ],
+ [
+ (/^(ox)en/i),
+ "$1"
+ ],
+ [
+ (/(alias|status)es$/i),
+ "$1"
+ ],
+ [
+ (/(octop|vir)i$/i),
+ "$1us"
+ ],
+ [
+ (/(cris|ax|test)es$/i),
+ "$1is"
+ ],
+ [
+ (/(shoe)s$/i),
+ "$1"
+ ],
+ [
+ (/(o)es$/i),
+ "$1"
+ ],
+ [
+ (/(bus)es$/i),
+ "$1"
+ ],
+ [
+ (/([m|l])ice$/i),
+ "$1ouse"
+ ],
+ [
+ (/(x|ch|ss|sh)es$/i),
+ "$1"
+ ],
+ [
+ (/(m)ovies$/i),
+ "$1ovie"
+ ],
+ [
+ (/(s)eries$/i),
+ "$1eries"
+ ],
+ [
+ (/([^aeiouy]|qu)ies$/i),
+ "$1y"
+ ],
+ [
+ (/([lr])ves$/i),
+ "$1f"
+ ],
+ [
+ (/(tive)s$/i),
+ "$1"
+ ],
+ [
+ (/(hive)s$/i),
+ "$1"
+ ],
+ [
+ (/([^f])ves$/i),
+ "$1fe"
+ ],
+ [
+ (/(^analy)ses$/i),
+ "$1sis"
+ ],
+ [
+ (/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i),
+ "$1$2sis"
+ ],
+ [
+ (/([ti])a$/i),
+ "$1um"
+ ],
+ [
+ (/(n)ews$/i),
+ "$1ews"
+ ],
+ [
+ (/(p)eople$/i),
+ "$1erson"
+ ],
+ [
+ (/s$/i),
+ ""
+ ]
+ ],
+ /* eslint-enable no-multi-spaces */
+ /**
+ * @private
+ * The registered uncountable words
+ * @property {String[]} uncountable
+ */
+ uncountable: [
+ "sheep",
+ "fish",
+ "series",
+ "species",
+ "money",
+ "rice",
+ "information",
+ "equipment",
+ "grass",
+ "mud",
+ "offspring",
+ "deer",
+ "means"
+ ],
+ /**
+ * Adds a new singularization rule to the Inflector. See the intro docs for more information
+ * @param {RegExp} matcher The matcher regex
+ * @param {String} replacer The replacement string, which can reference matches from the matcher
+ * argument
+ */
+ singular: function(matcher, replacer) {
+ this.singulars.unshift([
+ matcher,
+ replacer
+ ]);
+ },
+ /**
+ * Adds a new pluralization rule to the Inflector. See the intro docs for more information
+ * @param {RegExp} matcher The matcher regex
+ * @param {String} replacer The replacement string, which can reference matches from the matcher
+ * argument
+ */
+ plural: function(matcher, replacer) {
+ this.plurals.unshift([
+ matcher,
+ replacer
+ ]);
+ },
+ /**
+ * Removes all registered singularization rules
+ */
+ clearSingulars: function() {
+ this.singulars = [];
+ },
+ /**
+ * Removes all registered pluralization rules
+ */
+ clearPlurals: function() {
+ this.plurals = [];
+ },
+ /**
+ * Returns true if the given word is transnumeral (the word is its own singular and
+ * plural form - e.g. sheep, fish)
+ * @param {String} word The word to test
+ * @return {Boolean} True if the word is transnumeral
+ */
+ isTransnumeral: function(word) {
+ return Ext.Array.indexOf(this.uncountable, word) !== -1;
+ },
+ /**
+ * Returns the pluralized form of a word (e.g. Ext.util.Inflector.pluralize('word')
+ * returns 'words')
+ * @param {String} word The word to pluralize
+ * @return {String} The pluralized form of the word
+ */
+ pluralize: function(word) {
+ if (this.isTransnumeral(word)) {
+ return word;
+ }
+ // eslint-disable-next-line vars-on-top
+ var plurals = this.plurals,
+ length = plurals.length,
+ tuple, regex, i;
+ for (i = 0; i < length; i++) {
+ tuple = plurals[i];
+ regex = tuple[0];
+ // eslint-disable-next-line eqeqeq
+ if (regex == word || (regex.test && regex.test(word))) {
+ return word.replace(regex, tuple[1]);
+ }
+ }
+ return word;
+ },
+ /**
+ * Returns the singularized form of a word (e.g. Ext.util.Inflector.singularize('words')
+ * returns 'word')
+ * @param {String} word The word to singularize
+ * @return {String} The singularized form of the word
+ */
+ singularize: function(word) {
+ if (this.isTransnumeral(word)) {
+ return word;
+ }
+ // eslint-disable-next-line vars-on-top
+ var singulars = this.singulars,
+ length = singulars.length,
+ tuple, regex, i;
+ for (i = 0; i < length; i++) {
+ tuple = singulars[i];
+ regex = tuple[0];
+ // eslint-disable-next-line eqeqeq
+ if (regex == word || (regex.test && regex.test(word))) {
+ return word.replace(regex, tuple[1]);
+ }
+ }
+ return word;
+ },
+ /**
+ * Returns the correct {@link Ext.data.Model Model} name for a given string. Mostly used
+ * internally by the data
+ * package
+ * @param {String} word The word to classify
+ * @return {String} The classified version of the word
+ */
+ classify: function(word) {
+ return Ext.String.capitalize(this.singularize(word));
+ },
+ /**
+ * Ordinalizes a given number by adding a prefix such as 'st', 'nd', 'rd' or 'th' based on
+ * the last digit of the number. 21 -> 21st, 22 -> 22nd, 23 -> 23rd, 24 -> 24th etc
+ * @param {Number} number The number to ordinalize
+ * @return {String} The ordinalized number
+ */
+ ordinalize: function(number) {
+ var parsed = parseInt(number, 10),
+ mod10 = parsed % 10,
+ mod100 = parsed % 100;
+ // 11 through 13 are a special case
+ if (11 <= mod100 && mod100 <= 13) {
+ return number + "th";
+ } else {
+ switch (mod10) {
+ case 1:
+ return number + "st";
+ case 2:
+ return number + "nd";
+ case 3:
+ return number + "rd";
+ default:
+ return number + "th";
+ }
+ }
+ }
+}, function() {
+ // aside from the rules above, there are a number of words that have irregular pluralization
+ // so we add them here
+ var singular,
+ irregulars = {
+ alumnus: 'alumni',
+ cactus: 'cacti',
+ focus: 'foci',
+ nucleus: 'nuclei',
+ radius: 'radii',
+ stimulus: 'stimuli',
+ ellipsis: 'ellipses',
+ paralysis: 'paralyses',
+ oasis: 'oases',
+ appendix: 'appendices',
+ index: 'indexes',
+ beau: 'beaux',
+ bureau: 'bureaux',
+ tableau: 'tableaux',
+ woman: 'women',
+ child: 'children',
+ man: 'men',
+ corpus: 'corpora',
+ criterion: 'criteria',
+ curriculum: 'curricula',
+ genus: 'genera',
+ memorandum: 'memoranda',
+ phenomenon: 'phenomena',
+ foot: 'feet',
+ goose: 'geese',
+ tooth: 'teeth',
+ antenna: 'antennae',
+ formula: 'formulae',
+ nebula: 'nebulae',
+ vertebra: 'vertebrae',
+ vita: 'vitae'
+ };
+ for (singular in irregulars) {
+ if (irregulars.hasOwnProperty(singular)) {
+ this.plural(singular, irregulars[singular]);
+ this.singular(irregulars[singular], singular);
+ }
+ }
+});
+
+/**
+ * This class provides name derivation methods for use by a `Schema`.
+ *
+ * # Caching
+ *
+ * Because most name derivations are only textual manipulations of input strings, the
+ * results can be cached. This is handled by the `apply` method by giving it the name of
+ * the method to call. For example:
+ *
+ * var str = namer.capitalize('foo'); // = "Foo"
+ *
+ * var str = namer.apply('capitalize', 'foo');
+ *
+ * The return value of the second call (using `apply`) is the same as the first, however,
+ * the results of `capitalize` are cached. This allows repeated calls to `apply` given the
+ * same operation and string to avoid the extra string manipulation.
+ *
+ * # Usage
+ *
+ * This class is not intended to be created by application code. It is created by `Schema`
+ * instances as directed by the `namer` config. Application code can derive from this
+ * class and set the `namer` config to customize naming conventions used by the `Schema`.
+ *
+ * @protected
+ */
+Ext.define('Ext.data.schema.Namer', {
+ mixins: [
+ Ext.mixin.Factoryable
+ ],
+ alias: 'namer.default',
+ // also configures Factoryable
+ isNamer: true,
+ //-------------------------------------------------------------------------
+ // Cacheable methods
+ capitalize: function(name) {
+ return Ext.String.capitalize(name);
+ },
+ /**
+ * Given the name of a foreign key field, return the role of the related entity. For
+ * example, fields like "fooId" or "foo_id" this implementation returns "foo".
+ * @template
+ */
+ fieldRole: function(name) {
+ var match = name.match(this.endsWithIdRe, '');
+ if (match) {
+ name = name.substr(0, name.length - (match[1] || match[2]).length);
+ }
+ return this.apply('uncapitalize', name);
+ },
+ idField: function(name) {
+ // ex: User ==> userId
+ return this.apply('uncapitalize,singularize', name) + 'Id';
+ },
+ instanceName: function(roleName) {
+ return this.apply('underscore', roleName);
+ },
+ multiRole: function(name) {
+ return this.apply('undotted,uncapitalize,pluralize', name);
+ },
+ pluralize: function(name) {
+ return Ext.util.Inflector.pluralize(name);
+ },
+ readerRoot: function(roleName) {
+ return this.apply('uncapitalize', roleName);
+ },
+ singularize: function(name) {
+ return Ext.util.Inflector.singularize(name);
+ },
+ storeName: function(roleName) {
+ return this.apply('underscore', roleName);
+ },
+ uncapitalize: function(name) {
+ return Ext.String.uncapitalize(name);
+ },
+ underscore: function(name) {
+ return '_' + name;
+ },
+ uniRole: function(name) {
+ return this.apply('undotted,uncapitalize,singularize', name);
+ },
+ undotted: function(name) {
+ var parts, index;
+ if (name.indexOf('.') < 0) {
+ return name;
+ }
+ parts = name.split('.');
+ index = parts.length;
+ while (index-- > 1) {
+ parts[index] = this.apply('capitalize', parts[index]);
+ }
+ return parts.join('');
+ },
+ //-------------------------------------------------------------------------
+ // Non-Cacheable methods
+ getterName: function(role) {
+ var name = role.role;
+ if (role && role.isMany) {
+ // return this.apply('uncapitalize,pluralize', name);
+ return name;
+ }
+ // return this.apply('capitalize,singularize', name);
+ return 'get' + this.apply('capitalize', name);
+ },
+ inverseFieldRole: function(leftType, unique, rightRole, rightType) {
+ // In a FK association, the left side may be unique in which case we have a
+ // one-to-one otherwise we have a one-to-many. If the FK field is just the
+ // name of the right side class (e.g., if it is "order"), then we don't want
+ // to include the field name in the left role.
+ var me = this,
+ leftRole = me.apply(unique ? 'uniRole' : 'multiRole', leftType),
+ s1 = me.apply('pluralize', rightRole),
+ s2 = me.apply('undotted,pluralize', rightType);
+ if (s1.toLowerCase() !== s2.toLowerCase()) {
+ // Otherwise, we have something like "creatorId" on Ticket that holds a
+ // reference to User. This makes the right role "creator" so rather than
+ // make the left role "tickets" we make it "creatorTickets".
+ leftRole = rightRole + me.apply('capitalize', leftRole);
+ }
+ return leftRole;
+ },
+ manyToMany: function(relation, leftType, rightType) {
+ var me = this,
+ // ex: UserGroups
+ ret = me.apply('undotted,capitalize,singularize', leftType) + me.apply('undotted,capitalize,pluralize', rightType);
+ if (relation) {
+ ret = me.apply('capitalize', relation + ret);
+ }
+ return ret;
+ },
+ /**
+ * Returns the name for a one-to-many association given the left and right type and
+ * the associating `role`.
+ *
+ * In many cases the `role` matches the target type. For example, an OrderItem might
+ * have an "orderId" field which would have a `role` of "order". If this is a reference
+ * to an Order entity then the association name will be "OrderOrderItems".
+ *
+ * When the `role` does not match, it is included in the association name. For example,
+ * consider a Ticket entity with a "creatorId" field that references a User entity.
+ * The `role` of that field will (by default) be "creator". The returned association
+ * name will be "UserCreatorTickets".
+ */
+ manyToOne: function(leftType, leftRole, rightType, rightRole) {
+ // ex: OrderItem -> Order ==> OrderOrderItems
+ // Ticket (creator) -> User ==> UserCreatorTickets
+ return this.apply('capitalize,singularize', rightType) + this.apply('capitalize', leftRole);
+ },
+ matrixRole: function(relation, entityType) {
+ var ret = this.apply(relation ? 'multiRole,capitalize' : 'multiRole', entityType);
+ return relation ? relation + ret : ret;
+ },
+ oneToOne: function(leftType, leftRole, rightType, rightRole) {
+ return this.apply('undotted,capitalize,singularize', rightType) + this.apply('capitalize', leftRole);
+ },
+ setterName: function(role) {
+ return 'set' + this.apply('capitalize', role.role);
+ },
+ //-------------------------------------------------------------------------
+ // Private
+ endsWithIdRe: /(?:(_id)|[^A-Z](Id))$/,
+ cache: {},
+ apply: function(operation, name) {
+ var me = this,
+ cache = me.cache,
+ entry = cache[name] || (cache[name] = {}),
+ ret = entry[operation],
+ i, length, operations;
+ if (!ret) {
+ if (operation.indexOf(',') < 0) {
+ ret = me[operation](name);
+ } else {
+ length = (operations = operation.split(',')).length;
+ ret = name;
+ for (i = 0; i < length; ++i) {
+ ret = me.apply(operations[i], ret);
+ }
+ }
+ entry[operation] = ret;
+ }
+ return ret;
+ }
+});
+
+/**
+ * A Schema is a collection of related {@link Ext.data.Model entities} and their respective
+ * {@link Ext.data.schema.Association associations}.
+ *
+ * # Schema Instances
+ *
+ * By default a single instance of this class is created which serves as the schema for all
+ * entities that do not have an explicit `{@link Ext.data.Model#cfg-schema schema}` config
+ * either specified or inherited. This is sufficient in most cases.
+ *
+ * When an entity does specify a `{@link Ext.data.Model#cfg-schema schema}`, however, that
+ * looks up (or creates) an instance for that entity class which is then inherited.
+ *
+ * **Important:** All related entities *must* belong to a single schema instance in order
+ * to properly link up their associations.
+ *
+ * ## Configuring Schemas
+ *
+ * The best way to control the configuration of your `schema` is to define a base class for
+ * all of your entities and use the `{@link Ext.data.Model#cfg-schema schema}` config like
+ * this:
+ *
+ * Ext.define('MyApp.model.Base', {
+ * extend: 'Ext.data.Model',
+ *
+ * // This configures the default schema because we don't assign an "id":
+ * schema: {
+ * // configs go here
+ * }
+ * });
+ *
+ * **Note:** Only one explicit configuration can be applied to the default schema. In most
+ * applications this will not be an issue.
+ *
+ * By using a base class for your entities you can ensure that the default schema is fully
+ * configured before declaration of your classes proceeds. This is especially helpful if
+ * you need to set the `namespace` for your schema (see below).
+ *
+ * ## Relative Naming
+ *
+ * When describing associations between entities, it is desirable to use shorthand names
+ * that do not contain the common namespace portion. This is called the `entityName` as
+ * opposed to its class name. By default, the `entityName` is the full class name. However,
+ * if a namespace is used, the common portion can be discarded and we can derive a shorter name.
+ * In the following code, `"MyApp.model.Foo"` has an `entityName` of `"Foo"` and the schema has
+ * a `namespace` of "MyApp.model".
+ *
+ * If you use deeper nesting for entities, you may need to set the `namespace` config to
+ * account for this. For example:
+ *
+ * Ext.define('MyApp.model.Base', {
+ * extend: 'Ext.data.Model',
+ *
+ * schema: {
+ * namespace: 'MyApp.model'
+ * }
+ * });
+ *
+ * Your derived classes now will generate proper default `entityName` values even if they
+ * have further namespaces. For example, "MyApp.model.foo.Thing" will produce "foo.Thing"
+ * as the `entityName` given the above as a base class.
+ *
+ * # Association Naming
+ *
+ * There are various terms involved when describing associations. Perhaps the simplest
+ * example that will clarify these terms is that of the common many-to-many association
+ * of User and Group.
+ *
+ * * `entityName` - The names "User" and "Group" are the `entityName` values associated
+ * with these two classes. These are derived from their full classnames (perhaps
+ * something like "App.model.User" and "App.model.Group").
+ *
+ * * `associationName` - When talking about associations, especially the many-to-many
+ * variety, it is important to give them names. Associations are not owned by either of
+ * the entities involved, so this name is similar to an `entityName`. In the case of
+ * "User" and "Group", the default `associationName` would be "GroupUsers".
+ *
+ * * `left` and `right` - Associations describe a relationship between two entities. To
+ * talk about specific associations we would use the `entityName` of the parties (such
+ * as "User" or "Group"). When discussing associations in the abstract, however, it is
+ * very helpful to be able to talk about the entities in an association in a general way.
+ * In the case of the "GroupUsers" association, "User" is said to be the `left` while
+ * "Group" is said to be the `right`. In a many-to-many association the selection of
+ * `left` and `right` is arbitrary. When a foreign-key is involved, the `left` entity
+ * is the one containing the foreign-key.
+ *
+ * ## Custom Naming Conventions
+ *
+ * One of the jobs the the `Schema` is to manage name generation (such as `entityName`).
+ * This job is delegated to a class called the `namer`. If you need to generate names in
+ * other ways, you can provide a custom `namer` for your classes:
+ *
+ * Ext.define('MyApp.model.Base', {
+ * extend: 'Ext.data.Model',
+ *
+ * schema: {
+ * namespace: 'MyApp.model',
+ * namer: 'custom'
+ * }
+ * });
+ *
+ * This will create a class using the alias "namer.custom". For example:
+ *
+ * Ext.define('MyApp.model.CustomNamer', {
+ * extend: 'Ext.data.schema.Namer',
+ *
+ * alias: 'namer.custom',
+ * ...
+ * });
+ *
+ * For details see the documentation for {@link Ext.data.schema.Namer Namer}.
+ */
+Ext.define('Ext.data.schema.Schema', {
+ mixins: [
+ Ext.mixin.Factoryable
+ ],
+ alias: 'schema.default',
+ // also configures Factoryable
+ aliasPrefix: 'schema.',
+ isSchema: true,
+ /**
+ * @property {String} type
+ * The name of the schema's type. This should be the suffix of the `alias` for this
+ * class following the "schema." prefix. For example, if the `alias` for a schema is
+ * "schema.foo" then `type` should "foo". If an `alias` is specified on the derived
+ * class, this property is set automatically.
+ * @readonly
+ */
+ type: 'default',
+ statics: {
+ /**
+ * @property {Object} instances
+ * A collection of `Schema` instances keyed by its `type`.
+ *
+ * var mySchema = Ext.data.schema.Schema.instances.mySchema;
+ *
+ * If the `Schema` may not have been created yet, use the {@link #get} method to
+ * create the instance on first request:
+ *
+ * var mySchema = Ext.data.schema.Schema.get('mySchema');
+ *
+ * @readonly
+ * @private
+ */
+ instances: {},
+ // Method used for testing to clear cache for custom instances.
+ clearInstance: function(id) {
+ var schema = this.instances[id];
+ delete this.instances[id];
+ if (schema) {
+ schema.clear(true);
+ schema.destroy();
+ }
+ },
+ /**
+ * Returns the `Schema` instance given its `id` or config object. If only the `id`
+ * is specified, that `Schema` instance is looked up and returned. If there is no
+ * instance already created, the `id` is assumed to be the `type`. For example:
+ *
+ * schema: 'foo'
+ *
+ * Would be created from the alias `"schema.foo"` and assigned the `id` of "foo"
+ * as well.
+ *
+ * @param {String/Object} config The id, type or config object of the schema.
+ * @param {String} [config.type] The type alias of the schema. A "schema." prefix
+ * is added to this string, if provided, to complete the alias. This should match
+ * match the "alias" of some class derived from `Ext.data.schema.Schema`.
+ * @return {Ext.data.schema.Schema} The previously existing or newly created
+ * instance.
+ */
+ get: function(config) {
+ var Schema = this,
+ cache = Schema.instances,
+ id = 'default',
+ isString = config && Ext.isString(config),
+ instance, newConfig;
+ if (config) {
+ if (config.isSchema) {
+ return config;
+ }
+ id = isString ? config : (config.id || id);
+ }
+ if (!(instance = cache[id])) {
+ cache[id] = instance = Schema.create(config);
+ instance.id = id;
+ } else if (config && !isString) {
+ if (id !== 'default') {
+ Ext.raise('Only the default Schema instance can be reconfigured');
+ }
+ // When a Model contains a "schema" config object it is allowed to set the
+ // configuration of the default schema. This is the default behavior of
+ // this config on a model unless there is an "id" specified on it. So
+ // the trick is that we already have an instance so we want to merge the
+ // incoming config with the initial config of the default schema and then
+ // make that the effective initial config.
+ newConfig = Ext.merge({}, instance.config);
+ Ext.merge(newConfig, config);
+ instance.setConfig(newConfig);
+ instance.config = newConfig;
+ instance.setConfig = function() {
+ Ext.raise('The schema can only be reconfigured once');
+ };
+ }
+ return instance;
+ },
+ lookupEntity: function(entity) {
+ var ret = null,
+ instances = this.instances,
+ match, name, schema;
+ if (entity) {
+ if (entity.isEntity) {
+ ret = entity.self;
+ }
+ // a record
+ else if (Ext.isFunction(entity)) {
+ // A function (assume that a constructor is the Class).
+ ret = entity;
+ } else if (Ext.isString(entity)) {
+ ret = Ext.ClassManager.get(entity);
+ // If we've found a singleton or non-Entity class by that name, ignore it.
+ if (ret && (!ret.prototype || !ret.prototype.isEntity)) {
+ ret = null;
+ }
+ if (!ret) {
+ for (name in instances) {
+ schema = instances[name];
+ match = schema.getEntity(entity);
+ if (match) {
+ if (ret) {
+ Ext.raise('Ambiguous entity name "' + entity + '". Defined by schema "' + ret.schema.type + '" and "' + name + '"');
+ }
+ ret = match;
+ }
+ }
+ }
+ if (!ret) {
+ Ext.raise('No such Entity "' + entity + '".');
+ }
+ }
+ }
+ return ret;
+ }
+ },
+ /**
+ * @property {Number} assocCount The number of {@link Ext.data.schema.Association associations}
+ * in this `schema`.
+ * @readonly
+ */
+ assocCount: 0,
+ /**
+ * @property {Number} entityCount The number of {@link Ext.data.Model entities} in this
+ * `schema`.
+ * @readonly
+ */
+ entityCount: 0,
+ config: {
+ /**
+ * @cfg {Object} defaultIdentifier
+ * This config is used to initialize the `{@link Ext.data.Model#identifier}` config
+ * for classes that do not define one.
+ */
+ defaultIdentifier: null,
+ /**
+ * @cfg {Number} keyCheckDelay
+ * The time to wait (in ms) before checking for null foreign keys on records that
+ * will cause them to be dropped. This is useful for allowing records to be moved to
+ * a different source.
+ * @private
+ * @since 5.0.1
+ */
+ keyCheckDelay: 10,
+ /**
+ * @cfg {String/Object/Ext.data.schema.Namer} namer
+ * Specifies or configures the name generator for the schema.
+ */
+ namer: 'default',
+ /**
+ * @cfg {String} namespace
+ * The namespace for entity classes in this schema.
+ */
+ namespace: null,
+ /**
+ * @cfg {Object/Ext.util.ObjectTemplate} proxy
+ * This is a template used to produce `Ext.data.proxy.Proxy` configurations for
+ * Models that do not define an explicit `{@link Ext.data.Model#cfg-proxy proxy}`.
+ *
+ * This template is processed with the Model class as the data object which means
+ * any static properties of the Model are available. The most useful of these are
+ *
+ * * `prefix` - The `urlPrefix` property of this instance.
+ * * `entityName` - The {@link Ext.data.Model#entityName name} of the Model
+ * (for example, "User").
+ * * `schema` - This instance.
+ */
+ proxy: {
+ type: 'ajax',
+ url: '{prefix}/{entityName}'
+ },
+ /**
+ * @cfg {String} [urlPrefix=""]
+ * This is the URL prefix used for all requests to the server. It could be something
+ * like "/~api". This value is included in the `proxy` template data as "prefix".
+ */
+ urlPrefix: ''
+ },
+ onClassExtended: function(cls, data) {
+ var alias = data.alias;
+ if (alias && !data.type) {
+ if (!Ext.isString(alias)) {
+ alias = alias[0];
+ }
+ cls.prototype.type = alias.substring(this.prototype.aliasPrefix.length);
+ }
+ },
+ constructor: function(config) {
+ this.initConfig(config);
+ this.clear();
+ },
+ //-------------------------------------------------------------------------
+ // Config
+ //
+ applyDefaultIdentifier: function(identifier) {
+ return identifier && Ext.Factory.dataIdentifier(identifier);
+ },
+ applyNamer: function(namer) {
+ var ret = Ext.data.schema.Namer.create(namer);
+ ret.schema = this;
+ return ret;
+ },
+ applyNamespace: function(namespace) {
+ var end;
+ if (namespace) {
+ end = namespace.length - 1;
+ if (namespace.charAt(end) !== '.') {
+ namespace += '.';
+ }
+ }
+ return namespace;
+ },
+ applyProxy: function(proxy) {
+ return Ext.util.ObjectTemplate.create(proxy);
+ },
+ //
+ //-------------------------------------------------------------------------
+ // Public
+ eachAssociation: function(fn, scope) {
+ var associations = this.associations,
+ name;
+ for (name in associations) {
+ if (associations.hasOwnProperty(name)) {
+ if (fn.call(scope, name, associations[name]) === false) {
+ break;
+ }
+ }
+ }
+ },
+ eachEntity: function(fn, scope) {
+ var entities = this.entities,
+ name;
+ for (name in entities) {
+ if (entities.hasOwnProperty(name)) {
+ if (fn.call(scope, name, entities[name].cls) === false) {
+ break;
+ }
+ }
+ }
+ },
+ /**
+ * Returns an `Association` by name.
+ * @param {String} name The name of the association.
+ * @return {Ext.data.schema.Association} The association instance.
+ */
+ getAssociation: function(name) {
+ var entry = this.associations[name];
+ return entry || null;
+ },
+ /**
+ * Returns an entity by name.
+ * @param {String} name The name of the entity
+ * @return {Ext.data.Model} The entity class.
+ */
+ getEntity: function(name) {
+ var entry = this.entityClasses[name] || this.entities[name];
+ return (entry && entry.cls) || null;
+ },
+ /**
+ * Get the entity name taking into account the {@link #namespace}.
+ * @param {String/Ext.data.Model} cls The model class or name of the class.
+ * @return {String} The entity name
+ */
+ getEntityName: function(cls) {
+ var ns = this.getNamespace(),
+ index, name;
+ if (typeof cls === 'string') {
+ name = cls;
+ } else {
+ name = cls.$className || null;
+ }
+ if (name) {
+ // if (not anonymous class)
+ if (ns) {
+ index = ns.length;
+ if (name.substring(0, index) !== ns) {
+ return name;
+ }
+ }
+ if (index) {
+ name = name.substring(index);
+ }
+ }
+ return name;
+ },
+ /**
+ * Checks if the passed entity has attached associations that need to be read when
+ * using nested loading.
+ *
+ * @param {String/Ext.Class/Ext.data.Model} name The name, instance, or Model class.
+ * @return {Boolean} `true` if there are associations attached to the entity.
+ */
+ hasAssociations: function(name) {
+ name = name.entityName || name;
+ return !!this.associationEntityMap[name];
+ },
+ /**
+ * Checks if an entity is defined
+ * @param {String/Ext.data.Model} entity The name or model
+ * @return {Boolean} True if this entity is defined
+ */
+ hasEntity: function(entity) {
+ var name = this.getEntityName(entity);
+ return !!(this.entities[name] || this.entityClasses[name]);
+ },
+ //-------------------------------------------------------------------------
+ // Protected
+ /**
+ * Adds an entry from a {@link Ext.data.schema.ManyToMany matrix config} declared by an
+ * entity.
+ *
+ * This is the ideal method to override in a derived class if the standard, default
+ * naming conventions need to be adjusted. In the override, apply whatever logic is
+ * appropriate to determine the missing values and pass along the proper results to
+ * this method in the `callParent`.
+ *
+ * @param {Ext.Class} entityType A class derived from `Ext.data.Model`.
+ *
+ * @param {String} matrixName The name of the matrix association.
+ *
+ * @param {String} [relation] A base name for the matrix. For information about the
+ * meaning of this see {@link Ext.data.schema.Schema#ManyToMany}.
+ *
+ * @param {Object} left The descriptor for the "left" of the matrix.
+ * @param {String} left.type The type of the entity on the "left" of the matrix.
+ *
+ * @param {String} [left.field] The name of the field in the matrix table for the "left"
+ * side entity. If not provided, this defaults to the `left.type` name
+ * {@link Ext.util.Inflector#singularize singularized} and uncapitalized followed by
+ * "Id". For example, "userId" for a `left.type` of "Users".
+ *
+ * @param {String} [left.role] The name of the relationship from the `left.type` to the
+ * `right.type`. If not provided, this defaults to the `left.type` name
+ * {@link Ext.util.Inflector#pluralize pluralized} and uncapitalized. For example,
+ * "users" for a `left.type` of "User".
+ *
+ * @param {Object} right The descriptor for the "right" of the matrix.
+ * @param {String} right.type The type of the entity on the "right" of the matrix.
+ *
+ * @param {String} [right.field] The name of the field in the matrix table for the
+ * "right" side entity. If not provided, this defaults in the same way as `left.field`
+ * except this is based on `right.type`.
+ *
+ * @param {String} [right.role] The name of the relationship from the `right.type` to
+ * the `left.type`. If not provided, this defaults in the same way as `left.role`
+ * except this is based on `right.type`.
+ *
+ * @protected
+ */
+ addMatrix: function(entityType, matrixName, relation, left, right) {
+ var me = this,
+ namer = me.getNamer(),
+ associations = me.associations,
+ entities = me.entities,
+ leftType = left.type,
+ rightType = right.type,
+ leftField = left.field || namer.apply('idField', leftType),
+ rightField = right.field || namer.apply('idField', rightType),
+ leftRole = left.role || namer.matrixRole(relation, leftType),
+ rightRole = right.role || namer.matrixRole(relation, rightType),
+ matrix, leftEntry, rightEntry;
+ leftEntry = entities[leftType] || (entities[leftType] = {
+ cls: null,
+ name: leftType,
+ associations: {}
+ });
+ rightEntry = entities[rightType] || (entities[rightType] = {
+ cls: null,
+ name: rightType,
+ associations: {}
+ });
+ ++me.assocCount;
+ associations[matrixName] = matrix = new Ext.data.schema.ManyToMany({
+ name: matrixName,
+ schema: me,
+ definedBy: entityType,
+ left: {
+ cls: leftEntry.cls,
+ type: leftType,
+ role: leftRole,
+ field: leftField,
+ associationKey: left.associationKey
+ },
+ right: {
+ cls: rightEntry.cls,
+ type: rightType,
+ role: rightRole,
+ field: rightField,
+ associationKey: right.associationKey
+ }
+ });
+ leftEntry.associations[matrix.right.role] = matrix.right;
+ rightEntry.associations[matrix.left.role] = matrix.left;
+ if (leftEntry.cls) {
+ me.associationEntityMap[leftEntry.cls.entityName] = true;
+ }
+ if (rightEntry.cls) {
+ me.associationEntityMap[rightEntry.cls.entityName] = true;
+ }
+ me.decorateModel(matrix);
+ },
+ /**
+ * Adds a {@link Ext.data.Field#reference reference} field association for an entity
+ * to this `schema`.
+ *
+ * This is the ideal method to override in a derived class if the standard, default
+ * naming conventions need to be adjusted. In the override, apply whatever logic is
+ * appropriate to determine the missing values and pass along the proper results to
+ * this method in the `callParent`.
+ *
+ * @param {Ext.Class} entityType A class derived from `Ext.data.Model`.
+ *
+ * @param {Ext.data.field.Field} referenceField The `field` with the `reference` config.
+ *
+ * @param {Object} [descr] The `reference` descriptor from the `referenceField` if one
+ * was given in the field definition.
+ *
+ * @param {String} [descr.association] The name of the association. If empty or null, this
+ * will be derived from `entityType`, `role`, `inverse` and
+ * `referenceField.unique`.
+ *
+ * @param {String} [descr.role] The name of the relationship from `entityType` to the target
+ * `type`. If not specified, the default is the `referenceField.name` (minus any "Id"
+ * suffix if present).
+ *
+ * @param {String} [descr.inverse] The name of the relationship from the target `type`
+ * to the `entityType`. If not specified, this is derived from the
+ * {@link Ext.data.Model#entityName entityName} of the `entityType`
+ * ({@link Ext.util.Inflector#singularize singularized} or
+ * {@link Ext.util.Inflector#pluralize pluralized} based on `referenceField.unique`).
+ *
+ * @param {String} descr.type The {@link Ext.data.Model#entityName entityName} of the
+ * target of the reference.
+ *
+ * @param {Boolean} [unique=false] Indicates if the reference is one-to-one.
+ * @param {Boolean} [dupeCheck] (private)
+ *
+ * @protected
+ */
+ addReference: function(entityType, referenceField, descr, unique, dupeCheck) {
+ var me = this,
+ namer = me.getNamer(),
+ entities = me.entities,
+ associations = me.associations,
+ entityName = entityType.entityName,
+ association = descr.association,
+ child = descr.child,
+ parent = descr.parent,
+ rightRole = descr.role,
+ // Allow { child: 'OrderItem' } or the reverse (for one-to-one mostly):
+ rightType = descr.type || parent || child,
+ leftVal = descr.inverse,
+ left = Ext.isString(leftVal) ? {
+ role: leftVal
+ } : leftVal,
+ leftRole = left && left.role,
+ entry, T;
+ if (!rightRole) {
+ // In a FK association, the left side has the key in a field named something
+ // like "orderId". The default implementation of "fieldRole" namer is to drop
+ // the id suffix which gives is the role of the right side.
+ if (!referenceField || descr.legacy) {
+ rightRole = namer.apply('uncapitalize', rightType);
+ } else {
+ rightRole = namer.apply('fieldRole', referenceField.name);
+ }
+ }
+ if (!leftRole) {
+ leftRole = namer.inverseFieldRole(entityName, unique, rightRole, rightType);
+ }
+ if (!association) {
+ if (unique) {
+ association = namer.oneToOne(entityType, leftRole, rightType, rightRole);
+ } else {
+ association = namer.manyToOne(entityType, leftRole, rightType, rightRole);
+ }
+ }
+ if (dupeCheck && association in associations) {
+ if (dupeCheck(associations[association], association, leftRole, rightRole) !== false) {
+ return;
+ }
+ }
+ if (association in associations) {
+ Ext.raise('Duplicate association: "' + association + '" declared by ' + entityName + (referenceField ? ('.' + referenceField.name) : '') + ' (collides with ' + associations[association].definedBy.entityName + ')');
+ }
+ if (referenceField && referenceField.definedBy === entities[rightType]) {
+ Ext.raise('ForeignKey reference should not be owned by the target model');
+ }
+ // Lookup the entry for the target of the reference. Since it may not as yet be
+ // defined, we may need to create the entry.
+ entry = entities[rightType] || (entities[rightType] = {
+ cls: null,
+ name: rightType,
+ associations: {}
+ });
+ // as a field w/reference we are always "left":
+ T = unique ? Ext.data.schema.OneToOne : Ext.data.schema.ManyToOne;
+ association = new T({
+ name: association,
+ // Note: "parent" or "child" can be strings so don't assume otherwise
+ owner: child ? 'left' : (parent ? 'right' : null),
+ definedBy: entityType,
+ schema: me,
+ field: referenceField,
+ nullable: referenceField ? !!referenceField.allowBlank : true,
+ left: {
+ cls: entityType,
+ type: entityName,
+ role: leftRole,
+ extra: left
+ },
+ right: {
+ cls: entry.cls,
+ type: rightType,
+ role: rightRole,
+ extra: descr
+ },
+ meta: descr
+ });
+ // Add the left and right association "sides" to the appropriate collections, but
+ // remember that the right-side entity class may not yet be declared (that's ok as
+ // we store the associations in the entry):
+ entityType.associations[rightRole] = association.right;
+ entry.associations[leftRole] = association.left;
+ if (referenceField) {
+ // Store the role on the FK field. This "upgrades" legacy associations to the
+ // new "field.reference" form.
+ referenceField.reference = association.right;
+ entityType.references.push(referenceField);
+ }
+ ++me.assocCount;
+ me.associationEntityMap[entityName] = true;
+ if (entry.cls) {
+ me.associationEntityMap[entry.cls.entityName] = true;
+ }
+ associations[association.name] = association;
+ if (association.right.cls) {
+ me.decorateModel(association);
+ }
+ },
+ //-------------------------------------------------------------------------
+ privates: {
+ /**
+ * Adds an {@link Ext.data.Model entity} to this `schema`.
+ * @param {Ext.Class} entityType A class derived from {@link Ext.data.Model}.
+ * @private
+ */
+ addEntity: function(entityType) {
+ var me = this,
+ entities = me.entities,
+ entityName = entityType.entityName,
+ entry = entities[entityName],
+ fields = entityType.fields,
+ associations, field, i, length, name;
+ if (!entry) {
+ entities[entityName] = entry = {
+ name: entityName,
+ associations: {}
+ };
+ } else if (entry.cls) {
+ Ext.raise('Duplicate entity name "' + entityName + '": ' + entry.cls.$className + ' and ' + entityType.$className);
+ } else {
+ associations = entry.associations;
+ for (name in associations) {
+ // the associations collection describes the types to which this entity is
+ // related, but the inverse descriptors need this entityType:
+ associations[name].inverse.cls = entityType;
+ me.associationEntityMap[entityName] = true;
+ // We already have an entry, which means other associations have likely
+ // been added for us, so go ahead and do the inverse decoration
+ me.decorateModel(associations[name].association);
+ }
+ }
+ entry.cls = entityType;
+ entityType.prototype.associations = entityType.associations = entry.associations;
+ me.entityClasses[entityType.$className] = entry;
+ ++me.entityCount;
+ for (i = 0 , length = fields.length; i < length; ++i) {
+ field = fields[i];
+ if (field.reference) {
+ me.addReferenceDescr(entityType, field);
+ }
+ }
+ },
+ /**
+ * Adds the matrix associations of an {@link Ext.data.Model entity} to this `schema`.
+ * @param {Ext.Class} entityType A class derived from {@link Ext.data.Model Entity}.
+ * @param {Object/String[]} matrices The manyToMany matrices for the class.
+ * @private
+ */
+ addMatrices: function(entityType, matrices) {
+ var me = this,
+ i, length, matrixName;
+ if (Ext.isString(matrices)) {
+ me.addMatrixDescr(entityType, null, matrices);
+ } else if (matrices[0]) {
+ // if (isArray)
+ for (i = 0 , length = matrices.length; i < length; ++i) {
+ me.addMatrixDescr(entityType, null, matrices[i]);
+ }
+ } else {
+ for (matrixName in matrices) {
+ me.addMatrixDescr(entityType, matrixName, matrices[matrixName]);
+ }
+ }
+ },
+ /**
+ * Adds an entry from a {@link Ext.data.schema.ManyToMany matrix config} declared by an
+ * {@link Ext.data.Model entity}.
+ *
+ * @param {Ext.Class} entityType A class derived from {@link Ext.data.Model Entity}.
+ * @param {String} [matrixName] The name of the matrix association.
+ * @param {String/Object} matrixDef A {@link Ext.data.schema.ManyToMany matrix config}
+ * declared by an {@link Ext.data.Model entity}.
+ * @private
+ */
+ addMatrixDescr: function(entityType, matrixName, matrixDef) {
+ var me = this,
+ entityName = entityType.entityName,
+ associations = me.associations,
+ namer = me.getNamer(),
+ left = matrixDef.left,
+ right = matrixDef.right,
+ last, relation;
+ if (Ext.isString(matrixDef)) {
+ if (matrixDef.charAt(0) === '#') {
+ // "#User" (entity is on the left)
+ /*
+ * Ext.define('User', {
+ * extend: 'Ext.data.Model',
+ * manyToMany: '#Group'
+ * });
+ */
+ left = {
+ type: entityName
+ };
+ // User
+ right = {
+ type: matrixDef.substring(1)
+ };
+ }
+ // Group
+ else if (matrixDef.charAt(last = matrixDef.length - 1) === '#') {
+ // "User#"
+ /*
+ * Ext.define('Group', {
+ * extend: 'Ext.data.Model',
+ * manyToMany: 'User#'
+ * });
+ */
+ left = {
+ type: matrixDef.substring(0, last)
+ };
+ // User
+ right = {
+ type: entityName
+ };
+ }
+ // Group
+ else if (namer.apply('multiRole', entityName) < namer.apply('multiRole', matrixDef)) {
+ /*
+ * Ext.define('Group', {
+ * extend: 'Ext.data.Model',
+ * manyToMany: 'User'
+ * });
+ */
+ left = {
+ type: entityName
+ };
+ // Group
+ right = {
+ type: matrixDef
+ };
+ } else // User
+ {
+ /*
+ * Ext.define('User', {
+ * extend: 'Ext.data.Model',
+ * manyToMany: 'Group'
+ * });
+ */
+ left = {
+ type: matrixDef
+ };
+ // Group
+ right = {
+ type: entityName
+ };
+ }
+ } else // User
+ {
+ Ext.Assert.isString(matrixDef.type, 'No "type" for manyToMany in ' + entityName);
+ relation = matrixDef.relation;
+ /* eslint-disable-next-line max-len */
+ if (left || (!right && namer.apply('multiRole', entityName) < namer.apply('multiRole', matrixDef.type))) {
+ if (!left || left === true) {
+ /*
+ * Ext.define('User', {
+ * extend: 'Ext.data.Model',
+ * manyToMany: {
+ * type: 'Group',
+ * left: true
+ * }
+ * });
+ */
+ left = {
+ type: entityName
+ };
+ } else // User
+ {
+ /*
+ * Ext.define('User', {
+ * extend: 'Ext.data.Model',
+ * manyToMany: {
+ * type: 'Group',
+ * left: {
+ * role: 'useroids'
+ * }
+ * }
+ * });
+ */
+ left = Ext.apply({
+ type: entityName
+ }, left);
+ }
+ // User
+ right = matrixDef;
+ } else // Group
+ {
+ if (!right || right === true) {
+ /*
+ * Ext.define('Group', {
+ * extend: 'Ext.data.Model',
+ * manyToMany: {
+ * type: 'User',
+ * right: true
+ * }
+ * });
+ */
+ right = {
+ type: entityName
+ };
+ } else // Group
+ {
+ /*
+ * Ext.define('Group', {
+ * extend: 'Ext.data.Model',
+ * manyToMany: {
+ * type: 'User',
+ * right: {
+ * role: 'groupoids'
+ * }
+ * }
+ * });
+ */
+ right = Ext.apply({
+ type: entityName
+ }, right);
+ }
+ // Group
+ left = matrixDef;
+ }
+ }
+ // User
+ if (!matrixName) {
+ matrixName = namer.manyToMany(relation, left.type, right.type);
+ }
+ if (!(matrixName in associations)) {
+ me.addMatrix(entityType, matrixName, relation, left, right);
+ } else //
+ // In the case of a matrix association, both sides may need to declare it to allow
+ // them to be used w/o the other present. In development mode, we want to check
+ // that they declare the same thing!
+ //
+ {
+ /* eslint-disable-next-line vars-on-top, one-var */
+ var entry = associations[matrixName],
+ before = [
+ entry.kind,
+ entry.left.type,
+ entry.left.role,
+ entry.left.field,
+ entry.right.type,
+ entry.right.role,
+ entry.right.field
+ ].join('|'),
+ after;
+ // Call back in to bypass this check and realize the new association:
+ delete associations[matrixName];
+ me.addMatrix(entityType, matrixName, relation, left, right);
+ after = associations[matrixName];
+ // Restore the originals so we match production behavior (for testing)
+ associations[matrixName] = entry;
+ entry.left.cls.associations[entry.right.role] = entry.right;
+ entry.right.cls.associations[entry.left.role] = entry.left;
+ --me.assocCount;
+ // Now we can compare the old and the new to see if they are the same.
+ after = [
+ after.kind,
+ after.left.type,
+ after.left.role,
+ after.left.field,
+ after.right.type,
+ after.right.role,
+ after.right.field
+ ].join('|');
+ if (before != after) {
+ // eslint-disable-line eqeqeq
+ Ext.log.warn(matrixName + '(' + entry.definedBy.entityName + '): ' + before);
+ Ext.log.warn(matrixName + '(' + entityName + '): ' + after);
+ Ext.raise('Conflicting association: "' + matrixName + '" declared by ' + entityName + ' was previously declared by ' + entry.definedBy.entityName);
+ }
+ }
+ },
+ /**
+ * Adds a {@link Ext.data.Field#reference reference} {@link Ext.data.Field field}
+ * association for an entity to this `schema`. This method decodes the `reference`
+ * config of the `referenceField` and calls {@link #addReference}.
+ *
+ * @param {Ext.Class} entityType A class derived from {@link Ext.data.Model Model}.
+ * @param {Ext.data.Field} referenceField The `field` with the `reference` config.
+ * @private
+ */
+ addReferenceDescr: function(entityType, referenceField) {
+ var me = this,
+ descr = referenceField.$reference;
+ if (Ext.isString(descr)) {
+ descr = {
+ type: descr
+ };
+ } else {
+ descr = Ext.apply({}, descr);
+ }
+ me.addReference(entityType, referenceField, descr, referenceField.unique);
+ },
+ addBelongsTo: function(entityType, assoc) {
+ this.addKeylessSingle(entityType, assoc, false);
+ },
+ addHasOne: function(entityType, assoc) {
+ this.addKeylessSingle(entityType, assoc, true);
+ },
+ addKeylessSingle: function(entityType, assoc, unique) {
+ var foreignKey, referenceField;
+ assoc = Ext.apply({}, this.checkLegacyAssociation(entityType, assoc));
+ assoc.type = this.getEntityName(assoc.child || assoc.parent || assoc.type);
+ foreignKey = assoc.foreignKey || (assoc.type.toLowerCase() + '_id');
+ referenceField = entityType.getField(foreignKey);
+ assoc.fromSingle = true;
+ if (referenceField) {
+ referenceField.$reference = assoc;
+ referenceField.unique = true;
+ assoc.legacy = true;
+ Ext.log.warn('Using foreignKey is deprecated, use a keyed association. ' + 'See Ext.data.field.Field.reference');
+ }
+ this.addReference(entityType, referenceField, assoc, unique);
+ },
+ addHasMany: function(entityType, assoc) {
+ var me = this,
+ entities = me.entities,
+ pending = me.pending,
+ cls, name, referenceField, target, foreignKey, inverseOptions, child, declaredInverse;
+ assoc = Ext.apply({}, this.checkLegacyAssociation(entityType, assoc));
+ assoc.type = this.getEntityName(assoc.child || assoc.parent || assoc.type);
+ name = assoc.type;
+ target = entities[name];
+ cls = target && target.cls;
+ if (cls) {
+ name = entityType.entityName;
+ foreignKey = assoc.foreignKey || (name.toLowerCase() + '_id');
+ delete assoc.foreignKey;
+ // The assoc is really the inverse, so we only set the minimum.
+ // We copy the inverse from assoc and apply it over assoc!
+ declaredInverse = Ext.apply({}, assoc.inverse);
+ delete assoc.inverse;
+ inverseOptions = Ext.apply({}, assoc);
+ delete inverseOptions.type;
+ assoc = Ext.apply({
+ type: name,
+ inverse: inverseOptions
+ }, declaredInverse);
+ child = inverseOptions.child;
+ if (child) {
+ delete inverseOptions.child;
+ assoc.parent = name;
+ }
+ referenceField = cls.getField(foreignKey);
+ if (referenceField) {
+ referenceField.$reference = assoc;
+ assoc.legacy = true;
+ Ext.log.warn('Using foreignKey is deprecated, use a keyed association. ' + 'See Ext.data.field.Field.reference');
+ }
+ // We already have the entity, we can process it
+ me.addReference(cls, referenceField, assoc, false, /* eslint-disable-next-line comma-style */
+ function(association, name, leftRole, rightRole) {
+ // Check to see if the user has used belongsTo/hasMany in conjunction.
+ var result = !!association.meta.fromSingle && cls === association.left.cls,
+ l, r;
+ if (result) {
+ l = cls.entityName;
+ r = entityType.entityName;
+ Ext.raise('hasMany ("' + r + '") and belongsTo ("' + l + '") should not be used in conjunction to declare ' + 'a relationship. Use only one.');
+ }
+ return result;
+ });
+ } else {
+ // Pending, push it in the queue for when we load it
+ if (!pending[name]) {
+ pending[name] = [];
+ }
+ pending[name].push([
+ entityType,
+ assoc
+ ]);
+ }
+ },
+ checkLegacyAssociation: function(entityType, assoc) {
+ var name;
+ if (Ext.isString(assoc)) {
+ assoc = {
+ type: assoc
+ };
+ } else {
+ assoc = Ext.apply({}, assoc);
+ }
+ if (assoc.model) {
+ assoc.type = assoc.model;
+ // TODO: warn
+ delete assoc.model;
+ }
+ name = assoc.associatedName || assoc.name;
+ if (name) {
+ // TODO: warn
+ delete assoc.associatedName;
+ delete assoc.name;
+ assoc.role = name;
+ }
+ return assoc;
+ },
+ afterKeylessAssociations: function(cls) {
+ var pending = this.pending,
+ name = cls.entityName,
+ mine = pending[name],
+ i, len;
+ if (mine) {
+ for (i = 0 , len = mine.length; i < len; ++i) {
+ this.addHasMany.apply(this, mine[i]);
+ }
+ delete pending[name];
+ }
+ },
+ clear: function(clearNamespace) {
+ // for testing
+ var me = this,
+ timer = me.timer;
+ delete me.setConfig;
+ if (timer) {
+ window.clearTimeout(timer);
+ me.timer = null;
+ }
+ me.associations = {};
+ me.associationEntityMap = {};
+ me.entities = {};
+ me.entityClasses = {};
+ me.pending = {};
+ me.assocCount = me.entityCount = 0;
+ if (clearNamespace) {
+ me.setNamespace(null);
+ }
+ },
+ constructProxy: function(Model) {
+ var me = this,
+ data = Ext.Object.chain(Model),
+ proxy = me.getProxy();
+ data.schema = me;
+ data.prefix = me.getUrlPrefix();
+ return proxy.apply(data);
+ },
+ applyDecoration: function(role) {
+ var me = this,
+ // To decorate a role like "users" (of a User / Group matrix) we need to add
+ // getter/setter methods to access the "users" collection ... to Group! All
+ // other data about the "users" role and the User class belong to the given
+ // "role" but the receiver class is the inverse.
+ cls = role.inverse.cls,
+ namer = me.getNamer(),
+ getterName, setterName, proto;
+ // The cls may not be loaded yet, so we need to check if it is before
+ // we can decorate it.
+ if (cls && !role.decorated) {
+ role.decorated = true;
+ proto = cls.prototype;
+ if (!(getterName = role.getterName)) {
+ role.getterName = getterName = namer.getterName(role);
+ }
+ proto[getterName] = role.createGetter();
+ // Not all associations will create setters
+ if (role.createSetter) {
+ if (!(setterName = role.setterName)) {
+ role.setterName = setterName = namer.setterName(role);
+ }
+ proto[setterName] = role.createSetter();
+ }
+ }
+ },
+ decorateModel: function(association) {
+ this.applyDecoration(association.left);
+ this.applyDecoration(association.right);
+ },
+ processKeyChecks: function(processAll) {
+ var me = this,
+ keyCheckQueue = me.keyCheckQueue,
+ timer = me.timer,
+ len, i, item;
+ if (timer) {
+ window.clearTimeout(timer);
+ me.timer = null;
+ }
+ if (!keyCheckQueue) {
+ return;
+ }
+ // It's possible that processing a drop may cause another drop
+ // to occur. If we're trying to forcibly resolve the state, then
+ // we need to trigger all the drops at once. With processAll: false,
+ // the loop will jump out after the first iteration.
+ do {
+ keyCheckQueue = me.keyCheckQueue;
+ me.keyCheckQueue = [];
+ for (i = 0 , len = keyCheckQueue.length; i < len; ++i) {
+ item = keyCheckQueue[i];
+ item.role.checkKeyForDrop(item.record);
+ }
+ } while (processAll && me.keyCheckQueue.length);
+ },
+ queueKeyCheck: function(record, role) {
+ var me = this,
+ keyCheckQueue = me.keyCheckQueue,
+ timer = me.timer;
+ if (!keyCheckQueue) {
+ me.keyCheckQueue = keyCheckQueue = [];
+ }
+ keyCheckQueue.push({
+ record: record,
+ role: role
+ });
+ if (!timer) {
+ me.timer = timer = Ext.defer(me.processKeyChecks, me.getKeyCheckDelay(), me);
+ }
+ },
+ rankEntities: function() {
+ var me = this,
+ entities = me.entities,
+ entityNames = Ext.Object.getKeys(entities),
+ length = entityNames.length,
+ entityType, i;
+ me.nextRank = 1;
+ // We do an alpha sort to make the results more stable.
+ entityNames.sort();
+ for (i = 0; i < length; ++i) {
+ entityType = entities[entityNames[i]].cls;
+ if (!entityType.rank) {
+ me.rankEntity(entityType);
+ }
+ }
+ me.topoStack = null;
+ },
+ // cleanup diagnostic stack
+ rankEntity: function(entityType) {
+ var associations = entityType.associations,
+ associatedType, role, roleName;
+ /* eslint-disable-next-line vars-on-top, one-var */
+ var topoStack = this.topoStack || (this.topoStack = []),
+ entityName = entityType.entityName;
+ topoStack.push(entityName);
+ if (entityType.rank === 0) {
+ Ext.raise(entityName + " has circular foreign-key references: " + topoStack.join(" --> "));
+ }
+ entityType.rank = 0;
+ // mark as "adding" so we can detect cycles
+ for (roleName in associations) {
+ role = associations[roleName];
+ // The role describes the thing to which entityType is associated, so we
+ // want to know about *this* type and whether it has a foreign-key to the
+ // associated type. The left side is the FK owner so if the associated
+ // type is !left then entityType is left.
+ //
+ if (!role.left && role.association.field) {
+ // This entityType has a foreign-key to the associated type, so add
+ // that type first.
+ associatedType = role.cls;
+ if (!associatedType.rank) {
+ this.rankEntity(associatedType);
+ }
+ }
+ }
+ entityType.rank = this.nextRank++;
+ topoStack.pop();
+ }
+ }
+});
+// private
+
+/**
+ * AbstractStore is a superclass of {@link Ext.data.ProxyStore} and {@link Ext.data.ChainedStore}.
+ * It's never used directly, but offers a set of methods used by both of those subclasses.
+ *
+ * Unless you need to make a whole new type of Store, see {@link Ext.data.Store} instead.
+ */
+Ext.define('Ext.data.AbstractStore', {
+ mixins: [
+ Ext.mixin.Observable,
+ Ext.mixin.Factoryable
+ ],
+ factoryConfig: {
+ defaultType: 'store',
+ type: 'store'
+ },
+ $configPrefixed: false,
+ $configStrict: false,
+ config: {
+ /**
+ * @cfg {Object[]/Function[]} filters
+ * Array of {@link Ext.util.Filter Filters} for this store. Can also be passed array of
+ * functions which will be used as the {@link Ext.util.Filter#filterFn filterFn} config
+ * for filters:
+ *
+ * filters: [
+ * function(item) {
+ * return item.weight > 0;
+ * }
+ * ]
+ *
+ * Individual filters can be specified as an `Ext.util.Filter` instance, a config
+ * object for `Ext.util.Filter` or simply a function that will be wrapped in a
+ * instance with its {@link Ext.util.Filter#filterFn filterFn} set.
+ *
+ * For fine grain control of the filters collection, call `getFilters` to return
+ * the `Ext.util.Collection` instance that holds this store's filters.
+ *
+ * var filters = store.getFilters(); // an Ext.util.FilterCollection
+ *
+ * function legalAge (item) {
+ * return item.age >= 21;
+ * }
+ *
+ * filters.add(legalAge);
+ *
+ * //...
+ *
+ * filters.remove(legalAge);
+ *
+ * Any changes to the `filters` collection will cause this store to adjust
+ * its items accordingly.
+ */
+ filters: null,
+ /**
+ * @cfg {Boolean} [autoDestroy]
+ * When a Store is used by only one {@link Ext.view.View DataView}, and should only exist
+ * for the lifetime of that view, then configure the autoDestroy flag as `true`. This
+ * causes the destruction of the view to trigger the destruction of its Store.
+ */
+ autoDestroy: undefined,
+ /**
+ * @cfg {String} storeId
+ * Unique identifier for this store. If present, this Store will be registered with the
+ * {@link Ext.data.StoreManager}, making it easy to reuse elsewhere.
+ *
+ * Note that when a store is instantiated by a Controller, the storeId will default
+ * to the name of the store if not specified in the class.
+ */
+ storeId: null,
+ /**
+ * @cfg {Boolean} statefulFilters
+ * Configure as `true` to have the filters saved when a client {@link Ext.grid.Panel grid}
+ * saves its state.
+ */
+ statefulFilters: false,
+ /**
+ * @cfg {Ext.util.Sorter[]/Object[]} sorters
+ * The initial set of {@link Ext.util.Sorter Sorters}
+ *
+ * Individual sorters can be specified as an `Ext.util.Sorter` instance, a config
+ * object for `Ext.util.Sorter` or simply the name of a property by which to sort.
+ *
+ * An alternative way to extend the sorters is to call the `sort` method and pass
+ * a property or sorter config to add to the sorters.
+ *
+ * For fine grain control of the sorters collection, call `getSorters` to return
+ * the `Ext.util.Collection` instance that holds this collection's sorters.
+ *
+ * var sorters = store.getSorters(); // an Ext.util.SorterCollection
+ *
+ * sorters.add('name');
+ *
+ * //...
+ *
+ * sorters.remove('name');
+ *
+ * Any changes to the `sorters` collection will cause this store to adjust
+ * its items accordingly.
+ */
+ sorters: null,
+ /**
+ * @cfg {Boolean} [remoteSort=false]
+ * `true` if the sorting should be performed on the server side, false if it is local only.
+ */
+ remoteSort: {
+ lazy: true,
+ $value: false
+ },
+ /**
+ * @cfg {Boolean} [remoteFilter=false]
+ * `true` to defer any filtering operation to the server. If `false`, filtering is done
+ * locally on the client.
+ */
+ remoteFilter: {
+ lazy: true,
+ $value: false
+ },
+ /**
+ * @cfg {String} groupField
+ * The field by which to group data in the store. Internally, grouping is very similar to
+ * sorting - the groupField and {@link #groupDir} are injected as the first sorter
+ * (see {@link #method-sort}). Stores support a single level of grouping, and groups can be
+ * fetched via the {@link #getGroups} method.
+ */
+ groupField: undefined,
+ /**
+ * @cfg {String} groupDir
+ * The direction in which sorting should be applied when grouping. Supported values are
+ * "ASC" and "DESC".
+ */
+ groupDir: 'ASC',
+ /**
+ * @cfg {Object/Ext.util.Grouper} grouper
+ * The grouper by which to group the data store. May also be specified by the
+ * {@link #groupField} config, however
+ * they should not be used together.
+ */
+ grouper: null,
+ /**
+ * @cfg {Number} pageSize
+ * The number of records considered to form a 'page'. This is used to power the built-in
+ * paging using the nextPage and previousPage functions when the grid is paged using a
+ * {@link Ext.toolbar.Paging PagingToolbar} Defaults to 25.
+ *
+ * To disable paging, set the pageSize to `0`.
+ */
+ pageSize: 25,
+ /**
+ * @cfg {Boolean} [autoSort=true] `true` to maintain sorted order when records
+ * are added regardless of requested insertion point, or when an item mutation
+ * results in a new sort position.
+ *
+ * This does not affect a ChainedStore's reaction to mutations of the source
+ * Store. If sorters are present when the source Store is mutated, this ChainedStore's
+ * sort order will always be maintained.
+ * @private
+ */
+ autoSort: null,
+ /**
+ * @cfg {Boolean} reloadOnClearSorters
+ * Set this to `true` to trigger a reload when the last sorter is removed (only
+ * applicable when {@link #cfg!remoteSort} is `true`).
+ *
+ * By default, the store reloads itself when a sorter is added or removed.
+ *
+ * When the last sorter is removed, however, the assumption is that the data
+ * does not need to become "unsorted", and so no reload is triggered.
+ *
+ * If the server has a default order to which it reverts in the absence of any
+ * sorters, then it is useful to set this config to `true`.
+ * @since 6.5.1
+ */
+ reloadOnClearSorters: false
+ },
+ /**
+ * @property {Number} currentPage
+ * The page that the Store has most recently loaded
+ * (see {@link Ext.data.Store#loadPage loadPage})
+ */
+ currentPage: 1,
+ /**
+ * @property {Boolean} loading
+ * `true` if the Store is currently loading via its Proxy.
+ * @private
+ */
+ loading: false,
+ /**
+ * @property {Boolean} isStore
+ * `true` in this class to identify an object as an instantiated Store, or subclass thereof.
+ */
+ isStore: true,
+ /**
+ * @property {Number} updating
+ * A counter that is increased by `beginUpdate` and decreased by `endUpdate`. When
+ * this transitions from 0 to 1 the `{@link #event-beginupdate beginupdate}` event is
+ * fired. When it transitions back from 1 to 0 the `{@link #event-endupdate endupdate}`
+ * event is fired.
+ * @readonly
+ * @since 5.0.0
+ */
+ updating: 0,
+ constructor: function(config) {
+ var me = this,
+ storeId;
+ me.callParent([
+ config
+ ]);
+ /**
+ * @event add
+ * Fired when a Model instance has been added to this Store.
+ *
+ * @param {Ext.data.Store} store The store.
+ * @param {Ext.data.Model[]} records The records that were added.
+ * @param {Number} index The index at which the records were inserted.
+ * @since 1.1.0
+ */
+ /**
+ * @event remove
+ * Fired when one or more records have been removed from this Store.
+ *
+ * **The signature for this event has changed in 5.0:**
+ *
+ * @param {Ext.data.Store} store The Store object
+ * @param {Ext.data.Model[]} records The records that were removed. In previous
+ * releases this was a single record, not an array.
+ * @param {Number} index The index at which the records were removed.
+ * @param {Boolean} isMove `true` if the child node is being removed so it can be
+ * moved to another position in this Store.
+ * @since 5.0.0
+ */
+ /**
+ * @event update
+ * Fires when a Model instance has been updated.
+ * @param {Ext.data.Store} this
+ * @param {Ext.data.Model} record The Model instance that was updated
+ * @param {String} operation The update operation being performed. Value may be one of:
+ *
+ * Ext.data.Model.EDIT
+ * Ext.data.Model.REJECT
+ * Ext.data.Model.COMMIT
+ * @param {String[]} modifiedFieldNames Array of field names changed during edit.
+ * @param {Object} details An object describing the change. See the
+ * {@link Ext.util.Collection#event-itemchange itemchange event} of the store's backing
+ * collection
+ * @since 1.1.0
+ */
+ /**
+ * @event clear
+ * Fired after the {@link Ext.data.Store#removeAll removeAll} method is called.
+ * @param {Ext.data.Store} this
+ * @since 1.1.0
+ */
+ /**
+ * @event datachanged
+ * Fires for any data change in the store. This is a catch-all event that is typically fired
+ * in conjunction with other events (such as `add`, `remove`, `update`, `refresh`).
+ * @param {Ext.data.Store} this The data store
+ * @since 1.1.0
+ */
+ /**
+ * @event refresh
+ * Fires when the data cache has changed in a bulk manner (e.g., it has been sorted,
+ * filtered, etc.) and a widget that is using this Store as a Record cache should refresh
+ * its view.
+ * @param {Ext.data.Store} this The data store
+ */
+ /**
+ * @event beginupdate
+ * Fires when the {@link #beginUpdate} method is called. Automatic synchronization as
+ * configured by the {@link Ext.data.ProxyStore#autoSync autoSync} flag is deferred until
+ * the {@link #endUpdate} method is called, so multiple mutations can be coalesced into one
+ * synchronization operation.
+ */
+ /**
+ * @event endupdate
+ * Fires when the {@link #endUpdate} method is called. Automatic synchronization as
+ * configured by the {@link Ext.data.ProxyStore#autoSync autoSync} flag is deferred until
+ * the {@link #endUpdate} method is called, so multiple mutations can be coalesced into one
+ * synchronization operation.
+ */
+ /**
+ * @event beforesort
+ * Fires before a store is sorted.
+ *
+ * For {@link #remoteSort remotely sorted} stores, this will be just before the load
+ * operation triggered by changing the store's sorters.
+ *
+ * For locally sorted stores, this will be just before the data items in the store's
+ * backing collection are sorted.
+ * @param {Ext.data.Store} store The store being sorted
+ * @param {Ext.util.Sorter[]} sorters Array of sorters applied to the store
+ */
+ /**
+ * @event sort
+ * Fires after a store is sorted.
+ *
+ * For {@link #remoteSort remotely sorted} stores, this will be upon the success of a load
+ * operation triggered by changing the store's sorters.
+ *
+ * For locally sorted stores, this will be just after the data items in the store's backing
+ * collection are sorted.
+ * @param {Ext.data.Store} store The store being sorted
+ */
+ me.isInitializing = true;
+ me.mixins.observable.constructor.call(me, config);
+ me.isInitializing = false;
+ storeId = me.getStoreId();
+ if (!storeId && (config && config.id)) {
+ me.setStoreId(storeId = config.id);
+ }
+ if (storeId) {
+ Ext.data.StoreManager.register(me);
+ }
+ },
+ /**
+ * Create a `Range` instance to access records by their index.
+ *
+ * @param {Object/Ext.data.Range} [config]
+ * @return {Ext.data.Range}
+ * @since 6.5.0
+ */
+ createActiveRange: function(config) {
+ var range = Ext.apply({
+ store: this
+ }, config);
+ return new Ext.data.Range(range);
+ },
+ /**
+ * @private
+ * Called from onCollectionItemsAdd. Collection add changes the items reference of the
+ * collection, and that array object if directly referenced by Ranges. The ranges
+ * have to refresh themselves upon add.
+ */
+ syncActiveRanges: function() {
+ var activeRanges = this.activeRanges,
+ len = activeRanges && activeRanges.length,
+ i;
+ for (i = 0; i < len; i++) {
+ activeRanges[i].refresh();
+ }
+ },
+ /**
+ * Gets the number of records in store.
+ *
+ * If using paging, this may not be the total size of the dataset. If the data object used by
+ * the Reader contains the dataset size, then the {@link Ext.data.ProxyStore#getTotalCount}
+ * function returns the dataset size. **Note**: see the Important note in
+ * {@link Ext.data.ProxyStore#method-load}.
+ *
+ * When store is filtered, it's the number of records matching the filter.
+ *
+ * @return {Number} The number of Records in the Store.
+ */
+ getCount: function() {
+ var data = this.getData();
+ // We may be destroyed, in which case "data" will be null... best to just
+ // report 0 items vs throw an exception
+ return data ? data.getCount() : 0;
+ },
+ /**
+ * Determines if the passed range is available in the page cache.
+ * @private
+ * @param {Number} start The start index
+ * @param {Number} end The end index in the range
+ */
+ rangeCached: function(start, end) {
+ return this.getData().getCount() >= Math.max(start, end);
+ },
+ /**
+ * Checks if a record is in the current active data set.
+ * @param {Ext.data.Model} record The record
+ * @return {Boolean} `true` if the record is in the current active data set.
+ * @method contains
+ */
+ /**
+ * Finds the index of the first matching Record in this store by a specific field value.
+ *
+ * When store is filtered, finds records only within filter.
+ *
+ * **IMPORTANT**
+ *
+ * **If this store is {@link Ext.data.BufferedStore Buffered}, this can ONLY find records
+ * which happen to be cached in the page cache. This will be parts of the dataset around the
+ * currently visible zone, or recently visited zones if the pages have not yet been purged from
+ * the cache.**
+ *
+ * @param {String} property The name of the Record field to test.
+ * @param {String/RegExp} value Either a string that the field value
+ * should begin with, or a RegExp to test against the field.
+ * @param {Number} [startIndex=0] The index to start searching at
+ * @param {Boolean} [anyMatch=false] True to match any part of the string, not just the
+ * beginning.
+ * @param {Boolean} [caseSensitive=false] True for case sensitive comparison
+ * @param {Boolean} [exactMatch=false] True to force exact match (^ and $ characters
+ * added to the regex). Ignored if `anyMatch` is `true`.
+ * @return {Number} The matched index or -1
+ */
+ find: function(property, value, startIndex, anyMatch, caseSensitive, exactMatch) {
+ // exactMatch
+ // anyMatch F T
+ // F ^abc ^abc$
+ // T abc abc
+ //
+ var startsWith = !anyMatch,
+ endsWith = !!(startsWith && exactMatch);
+ return this.getData().findIndex(property, value, startIndex, startsWith, endsWith, !caseSensitive);
+ },
+ /**
+ * Finds the first matching Record in this store by a specific field value.
+ *
+ * When store is filtered, finds records only within filter.
+ *
+ * **IMPORTANT**
+ *
+ * **If this store is {@link Ext.data.BufferedStore Buffered}, this can ONLY find records which
+ * happen to be cached in the page cache. This will be parts of the dataset around the
+ * currently visible zone, or recently visited zones if the pages have not yet been purged
+ * from the cache.**
+ *
+ * @param {String} fieldName The name of the Record field to test.
+ * @param {String/RegExp} value Either a string that the field value
+ * should begin with, or a RegExp to test against the field.
+ * @param {Number} [startIndex=0] The index to start searching at
+ * @param {Boolean} [anyMatch=false] True to match any part of the string, not just the
+ * beginning.
+ * @param {Boolean} [caseSensitive=false] True for case sensitive comparison
+ * @param {Boolean} [exactMatch=false] True to force exact match (^ and $ characters
+ * added to the regex). Ignored if `anyMatch` is `true`.
+ * @return {Ext.data.Model} The matched record or null
+ */
+ findRecord: function() {
+ var me = this,
+ index = me.find.apply(me, arguments);
+ return index !== -1 ? me.getAt(index) : null;
+ },
+ /**
+ * Finds the index of the first matching Record in this store by a specific field value.
+ *
+ * When store is filtered, finds records only within filter.
+ *
+ * **IMPORTANT**
+ *
+ * **If this store is {@link Ext.data.BufferedStore Buffered}, this can ONLY find records which
+ * happen to be cached in the page cache. This will be parts of the dataset around the
+ * currently visible zone, or recently visited zones if the pages have not yet been purged
+ * from the cache.**
+ *
+ * @param {String} fieldName The name of the Record field to test.
+ * @param {Object} value The value to match the field against.
+ * @param {Number} [startIndex=0] The index to start searching at
+ * @return {Number} The matched index or -1
+ */
+ findExact: function(fieldName, value, startIndex) {
+ return this.getData().findIndexBy(function(rec) {
+ return rec.isEqual(rec.get(fieldName), value);
+ }, this, startIndex);
+ },
+ /**
+ * Find the index of the first matching Record in this Store by a function.
+ * If the function returns `true` it is considered a match.
+ *
+ * When store is filtered, finds records only within filter.
+ *
+ * **IMPORTANT**
+ *
+ * **If this store is {@link Ext.data.BufferedStore Buffered}, this can ONLY find records which
+ * happen to be cached in the page cache. This will be parts of the dataset around the
+ * currently visible zone, or recently visited zones if the pages have not yet been purged
+ * from the cache.**
+ *
+ * @param {Function} fn The function to be called. It will be passed the following parameters:
+ * @param {Ext.data.Model} fn.record The record to test for filtering. Access field values
+ * using {@link Ext.data.Model#get}.
+ * @param {Object} fn.id The ID of the Record passed.
+ * @param {Object} [scope] The scope (this reference) in which the function is executed.
+ * Defaults to this Store.
+ * @param {Number} [start=0] The index at which to start searching.
+ * @return {Number} The matched index or -1
+ */
+ findBy: function(fn, scope, start) {
+ return this.getData().findIndexBy(fn, scope, start);
+ },
+ /**
+ * Get the Record at the specified index.
+ *
+ * The index is effected by filtering.
+ *
+ * @param {Number} index The index of the Record to find.
+ * @return {Ext.data.Model} The Record at the passed index. Returns null if not found.
+ */
+ getAt: function(index) {
+ return this.getData().getAt(index) || null;
+ },
+ /**
+ * Gathers a range of Records between specified indices.
+ *
+ * This method is affected by filtering.
+ *
+ * @param {Number} start The starting index. Defaults to zero.
+ * @param {Number} end The ending index. Defaults to the last record. The end index
+ * **is included**.
+ * @param [options] (private) Used by BufferedRenderer when using a BufferedStore.
+ * @return {Ext.data.Model[]} An array of records.
+ */
+ getRange: function(start, end, options) {
+ // Collection's getRange is exclusive. Do NOT mutate the value: it is passed to the
+ // callback.
+ var result = this.getData().getRange(start, Ext.isNumber(end) ? end + 1 : end);
+ // BufferedRenderer requests a range with a callback to process that range.
+ // Because it may be dealing with a buffered store and the range may not be available
+ // synchronously.
+ if (options && options.callback) {
+ options.callback.call(options.scope || this, result, start, end, options);
+ }
+ return result;
+ },
+ /**
+ * Gets the filters for this store.
+ * @param {Boolean} [autoCreate] (private)
+ * @return {Ext.util.FilterCollection} The filters
+ */
+ getFilters: function(autoCreate) {
+ var me = this,
+ result = me.callParent();
+ if (!result && autoCreate !== false) {
+ me.setFilters([]);
+ result = me.callParent();
+ }
+ return result;
+ },
+ applyFilters: function(filters, filtersCollection) {
+ var me = this,
+ created;
+ if (!filtersCollection) {
+ filtersCollection = me.createFiltersCollection();
+ created = true;
+ }
+ filtersCollection.add(filters);
+ if (created) {
+ me.onRemoteFilterSet(filtersCollection, me.getRemoteFilter());
+ }
+ return filtersCollection;
+ },
+ /**
+ * Gets the sorters for this store.
+ * @param {Boolean} [autoCreate] (private)
+ * @return {Ext.util.SorterCollection} The sorters
+ */
+ getSorters: function(autoCreate) {
+ var me = this,
+ result = me.callParent();
+ if (!result && autoCreate !== false) {
+ // If not preventing creation, force it here
+ me.setSorters([]);
+ result = me.callParent();
+ }
+ return result;
+ },
+ applySorters: function(sorters, sortersCollection) {
+ var me = this,
+ created;
+ if (!sortersCollection) {
+ sortersCollection = me.createSortersCollection();
+ created = true;
+ }
+ sortersCollection.add(sorters);
+ if (created) {
+ me.onRemoteSortSet(sortersCollection, me.getRemoteSort());
+ }
+ return sortersCollection;
+ },
+ /**
+ * Filters the data in the Store by one or more fields. Example usage:
+ *
+ * //filter with a single field
+ * myStore.filter('firstName', 'Don');
+ *
+ * //filtering with multiple filters
+ * myStore.filter([
+ * {
+ * property : 'firstName',
+ * value : 'Don'
+ * },
+ * {
+ * property : 'lastName',
+ * value : 'Griffin'
+ * }
+ * ]);
+ *
+ * Internally, Store converts the passed arguments into an array of
+ * {@link Ext.util.Filter} instances, and delegates the actual filtering to its internal
+ * {@link Ext.util.Collection} or the remote server.
+ *
+ * @param {String/Ext.util.Filter[]} [filters] Either a string name of one of the
+ * fields in this Store's configured {@link Ext.data.Model Model}, or an array of
+ * filter configurations.
+ * @param {String} [value] The property value by which to filter. Only applicable if
+ * `filters` is a string.
+ * @param {Boolean} [suppressEvent] (private)
+ */
+ filter: function(filters, value, suppressEvent) {
+ if (Ext.isString(filters)) {
+ filters = {
+ property: filters,
+ value: value
+ };
+ }
+ this.suppressNextFilter = !!suppressEvent;
+ this.getFilters().add(filters);
+ this.suppressNextFilter = false;
+ },
+ /**
+ * Removes an individual Filter from the current {@link #cfg-filters filter set}
+ * using the passed Filter/Filter id and by default, applies the updated filter set
+ * to the Store's unfiltered dataset.
+ *
+ * @param {String/Ext.util.Filter} toRemove The id of a Filter to remove from the
+ * filter set, or a Filter instance to remove.
+ * @param {Boolean} [suppressEvent] If `true` the filter is cleared silently.
+ */
+ removeFilter: function(toRemove, suppressEvent) {
+ var me = this,
+ filters = me.getFilters();
+ me.suppressNextFilter = !!suppressEvent;
+ if (toRemove instanceof Ext.util.Filter) {
+ filters.remove(toRemove);
+ } else {
+ filters.removeByKey(toRemove);
+ }
+ me.suppressNextFilter = false;
+ },
+ updateAutoSort: function(autoSort) {
+ // Keep collection synced with our autoSort setting
+ this.getData().setAutoSort(autoSort);
+ },
+ updateRemoteSort: function(remoteSort) {
+ // Don't call the getter here, we don't want to force sorters to be created here.
+ // Also, applySorters calls getRemoteSort, which may trigger the initGetter.
+ this.onRemoteSortSet(this.getSorters(false), remoteSort);
+ },
+ updateRemoteFilter: function(remoteFilter) {
+ this.onRemoteFilterSet(this.getFilters(false), remoteFilter);
+ },
+ /**
+ * Adds a new Filter to this Store's {@link #cfg-filters filter set} and
+ * by default, applies the updated filter set to the Store's unfiltered dataset.
+ * @param {Object[]/Ext.util.Filter[]} filters The set of filters to add to the current
+ * {@link #cfg-filters filter set}.
+ * @param {Boolean} [suppressEvent] If `true` the filter is cleared silently.
+ */
+ addFilter: function(filters, suppressEvent) {
+ this.suppressNextFilter = !!suppressEvent;
+ this.getFilters().add(filters);
+ this.suppressNextFilter = false;
+ },
+ /**
+ * Filters by a function. The specified function will be called for each
+ * Record in this Store. If the function returns `true` the Record is included,
+ * otherwise it is filtered out.
+ *
+ * When store is filtered, most of the methods for accessing store data will be working only
+ * within the set of filtered records. The notable exception is {@link #getById}.
+ *
+ * @param {Function} fn The function to be called. It will be passed the following parameters:
+ * @param {Ext.data.Model} fn.record The record to test for filtering. Access field values
+ * using {@link Ext.data.Model#get}.
+ * @param {Object} [scope] The scope (this reference) in which the function is executed.
+ * Defaults to this Store.
+ */
+ filterBy: function(fn, scope) {
+ this.getFilters().add({
+ filterFn: fn,
+ scope: scope || this
+ });
+ },
+ /**
+ * Reverts to a view of the Record cache with no filtering applied.
+ * @param {Boolean} [suppressEvent] If `true` the filter is cleared silently.
+ *
+ * For a locally filtered Store, this means that the filter collection is cleared without
+ * firing the {@link #datachanged} event.
+ *
+ * For a remotely filtered Store, this means that the filter collection is cleared, but
+ * the store is not reloaded from the server.
+ */
+ clearFilter: function(suppressEvent) {
+ var me = this,
+ filters = me.getFilters(false);
+ if (!filters || filters.getCount() === 0) {
+ return;
+ }
+ me.suppressNextFilter = !!suppressEvent;
+ filters.removeAll();
+ me.suppressNextFilter = false;
+ },
+ /**
+ * Tests whether the store currently has any active filters.
+ * @return {Boolean} `true` if the store is filtered.
+ */
+ isFiltered: function() {
+ return this.getFilters().getCount() > 0;
+ },
+ /**
+ * Tests whether the store currently has any active sorters.
+ * @return {Boolean} `true` if the store is sorted.
+ */
+ isSorted: function() {
+ var sorters = this.getSorters(false);
+ return !!(sorters && sorters.length > 0) || this.isGrouped();
+ },
+ addFieldTransform: function(sorter) {
+ // Transform already specified, leave it
+ if (sorter.getTransform()) {
+ return;
+ }
+ /* eslint-disable-next-line vars-on-top */
+ var fieldName = sorter.getProperty(),
+ Model = this.getModel(),
+ field, sortType;
+ if (Model) {
+ field = Model.getField(fieldName);
+ sortType = field ? field.getSortType() : null;
+ }
+ if (sortType && sortType !== Ext.identityFn) {
+ sorter.setTransform(sortType);
+ }
+ },
+ /**
+ * This method may be called to indicate the start of multiple changes to the store.
+ *
+ * Automatic synchronization as configured by the {@link Ext.data.ProxyStore#autoSync autoSync}
+ * flag is deferred until the {@link #endUpdate} method is called, so multiple mutations can be
+ * coalesced into one synchronization operation.
+ *
+ * Internally this method increments a counter that is decremented by `endUpdate`. It
+ * is important, therefore, that if you call `beginUpdate` directly you match that
+ * call with a call to `endUpdate` or you will prevent the collection from updating
+ * properly.
+ *
+ * For example:
+ *
+ * var store = Ext.StoreManager.lookup({
+ * //...
+ * autoSync: true
+ * });
+ *
+ * store.beginUpdate();
+ *
+ * record.set('fieldName', 'newValue');
+ *
+ * store.add(item);
+ * // ...
+ *
+ * store.insert(index, otherItem);
+ * //...
+ *
+ * // Interested parties will listen for the endupdate event
+ * store.endUpdate();
+ *
+ * @since 5.0.0
+ */
+ beginUpdate: function() {
+ if (!this.updating++ && this.hasListeners.beginupdate) {
+ this.fireEvent('beginupdate');
+ }
+ },
+ /**
+ * This method is called after modifications are complete on a store. For details
+ * see `{@link #beginUpdate}`.
+ * @since 5.0.0
+ */
+ endUpdate: function() {
+ if (this.updating && !--this.updating) {
+ if (this.hasListeners.endupdate) {
+ this.fireEvent('endupdate');
+ }
+ this.onEndUpdate();
+ }
+ },
+ /**
+ * @private
+ * Returns the grouping, sorting and filtered state of this Store.
+ */
+ getState: function() {
+ var me = this,
+ sorters = [],
+ filters = me.getFilters(),
+ grouper = me.getGrouper(),
+ filterState, hasState, result;
+ // Create sorters config array.
+ me.getSorters().each(function(s) {
+ sorters[sorters.length] = s.getState();
+ hasState = true;
+ });
+ // Because we do not provide a filter changing mechanism, only statify the filters if they
+ // opt in. Otherwise filters would get "stuck".
+ if (me.statefulFilters && me.saveStatefulFilters) {
+ // If saveStatefulFilters is turned on then we know that the filter collection has
+ // changed since page load. Initiate the filterState as an empty stack, which is
+ // meaningful in itself. If there are any filter in the collection, persist them.
+ hasState = true;
+ filterState = [];
+ filters.each(function(f) {
+ filterState[filterState.length] = f.getState();
+ });
+ }
+ if (grouper) {
+ hasState = true;
+ }
+ // If there is any state to save, return it as an object
+ if (hasState) {
+ result = {};
+ if (sorters.length) {
+ result.sorters = sorters;
+ }
+ if (filterState) {
+ result.filters = filterState;
+ }
+ if (grouper) {
+ result.grouper = grouper.getState();
+ }
+ }
+ return result;
+ },
+ /**
+ * @private
+ * Restores state to the passed state
+ */
+ applyState: function(state) {
+ var me = this,
+ stateSorters = state.sorters,
+ stateFilters = state.filters,
+ stateGrouper = state.grouper;
+ if (stateSorters) {
+ me.getSorters().replaceAll(stateSorters);
+ }
+ if (stateFilters) {
+ // We found persisted filters so let's save stateful filters from this point forward.
+ me.saveStatefulFilters = true;
+ me.getFilters().replaceAll(stateFilters);
+ }
+ if (stateGrouper) {
+ me.setGrouper(stateGrouper);
+ }
+ },
+ /**
+ * Get the Record with the specified id.
+ *
+ * This method is not affected by filtering, lookup will be performed from all records
+ * inside the store, filtered or not.
+ *
+ * @param {Mixed} id The id of the Record to find.
+ * @return {Ext.data.Model} The Record with the passed id. Returns null if not found.
+ * @method getById
+ */
+ /**
+ * Returns true if the store has a pending load task.
+ * @return {Boolean} `true` if the store has a pending load task.
+ * @private
+ * @method
+ */
+ hasPendingLoad: Ext.emptyFn,
+ /**
+ * Returns `true` if the Store has been loaded.
+ * @return {Boolean} `true` if the Store has been loaded.
+ * @method
+ */
+ isLoaded: Ext.emptyFn,
+ /**
+ * Returns `true` if the Store is currently performing a load operation.
+ * @return {Boolean} `true` if the Store is currently loading.
+ * @method
+ */
+ isLoading: Ext.emptyFn,
+ destroy: function() {
+ var me = this;
+ if (me.hasListeners.beforedestroy) {
+ me.fireEvent('beforedestroy', me);
+ }
+ me.destroying = true;
+ if (me.getStoreId()) {
+ Ext.data.StoreManager.unregister(me);
+ }
+ me.doDestroy();
+ if (me.hasListeners.destroy) {
+ me.fireEvent('destroy', me);
+ }
+ // This just makes it hard to ask "was destroy() called?":
+ // me.destroying = false; // removed in 7.0
+ // This will finish the sequence and null object references
+ me.callParent();
+ },
+ /**
+ * Perform the Store destroying sequence. Override this method to add destruction
+ * behaviors to your custom Stores.
+ *
+ */
+ doDestroy: Ext.emptyFn,
+ /**
+ * Sorts the data in the Store by one or more of its properties. Example usage:
+ *
+ * //sort by a single field
+ * myStore.sort('myField', 'DESC');
+ *
+ * //sorting by multiple fields
+ * myStore.sort([
+ * {
+ * property : 'age',
+ * direction: 'ASC'
+ * },
+ * {
+ * property : 'name',
+ * direction: 'DESC'
+ * }
+ * ]);
+ *
+ * Internally, Store converts the passed arguments into an array of {@link Ext.util.Sorter}
+ * instances, and either delegates the actual sorting to its internal
+ * {@link Ext.util.Collection} or the remote server.
+ *
+ * When passing a single string argument to sort, Store maintains a ASC/DESC toggler per field,
+ * so this code:
+ *
+ * store.sort('myField');
+ * store.sort('myField');
+ *
+ * Is equivalent to this code, because Store handles the toggling automatically:
+ *
+ * store.sort('myField', 'ASC');
+ * store.sort('myField', 'DESC');
+ *
+ * @param {String/Ext.util.Sorter[]} [field] Either a string name of one of the
+ * fields in this Store's configured {@link Ext.data.Model Model}, or an array of
+ * sorter configurations.
+ * @param {"ASC"/"DESC"} [direction="ASC"] The overall direction to sort the data by.
+ * @param {"append"/"prepend"/"replace"/"multi"} [mode="replace"]
+ */
+ sort: function(field, direction, mode) {
+ var me = this;
+ if (arguments.length === 0) {
+ if (me.getRemoteSort()) {
+ me.load();
+ } else {
+ me.forceLocalSort();
+ }
+ } else {
+ me.getSorters().addSort(field, direction, mode);
+ }
+ },
+ // This is attached to the data Collection's beforesort event only if not remoteSort
+ // If remoteSort, the event is fired before the reload call in Ext.data.ProxyStore#load.
+ onBeforeCollectionSort: function(store, sorters) {
+ if (sorters) {
+ this.fireEvent('beforesort', this, sorters.getRange());
+ }
+ },
+ onSorterEndUpdate: function() {
+ var me = this,
+ fireSort = true,
+ sorters = me.getSorters(false),
+ sorterCount;
+ // If we're in the middle of grouping, it will take care of loading.
+ // If the collection is not instantiated yet, it's because we are constructing.
+ if (me.settingGroups || !sorters) {
+ return;
+ }
+ sorters = sorters.getRange();
+ sorterCount = sorters.length;
+ if (me.getRemoteSort()) {
+ // Only reload if there are sorters left to influence the sort order.
+ // Unless reloadOnClearSorters is set to indicate that there's a default
+ // order used by the server which must be returned to when there is no
+ // explicit sort order.
+ if (sorters.length || me.getReloadOnClearSorters()) {
+ // The sort event will fire in the load callback;
+ fireSort = false;
+ me.load({
+ callback: function() {
+ me.fireEvent('sort', me, sorters);
+ }
+ });
+ }
+ } else if (sorterCount) {
+ me.fireEvent('datachanged', me);
+ me.fireEvent('refresh', me);
+ }
+ if (fireSort) {
+ // Sort event must fire when sorters collection is updated to empty.
+ me.fireEvent('sort', me, sorters);
+ }
+ },
+ onFilterEndUpdate: function() {
+ var me = this,
+ suppressNext = me.suppressNextFilter,
+ filters = me.getFilters(false);
+ // If the collection is not instantiated yet, it's because we are constructing.
+ if (!filters) {
+ return;
+ }
+ if (me.getRemoteFilter()) {
+ me.getFilters().each(function(filter) {
+ if (filter.getInitialConfig().filterFn) {
+ Ext.raise('Unable to use a filtering function in conjunction with ' + 'remote filtering.');
+ }
+ });
+ me.currentPage = 1;
+ if (!suppressNext) {
+ me.load();
+ }
+ } else if (!suppressNext) {
+ me.fireEvent('datachanged', me);
+ me.fireEvent('refresh', me);
+ }
+ if (me.trackStateChanges) {
+ // We just mutated the filter collection so let's save stateful filters
+ // from this point forward.
+ me.saveStatefulFilters = true;
+ }
+ // This is not affected by suppressEvent.
+ me.fireEvent('filterchange', me, me.getFilters().getRange());
+ },
+ updateGroupField: function(field) {
+ if (field) {
+ this.setGrouper({
+ property: field,
+ direction: this.getGroupDir()
+ });
+ } else {
+ this.setGrouper(null);
+ }
+ },
+ /**
+ * @method setFilters
+ */
+ /**
+ * @method setSorters
+ */
+ getGrouper: function() {
+ return this.getData().getGrouper();
+ },
+ /**
+ * Groups data inside the store.
+ * @param {String/Object} grouper Either a string name of one of the fields in this Store's
+ * configured {@link Ext.data.Model Model}, or an object, or a {@link Ext.util.Grouper grouper}
+ * configuration object.
+ * @param {String} [direction] The overall direction to group the data by. Defaults to the
+ * value of {@link #groupDir}.
+ */
+ group: function(grouper, direction) {
+ var me = this,
+ sorters = me.getSorters(false),
+ change = grouper || (sorters && sorters.length),
+ data = me.getData();
+ if (grouper && typeof grouper === 'string') {
+ grouper = {
+ property: grouper,
+ direction: direction || me.getGroupDir()
+ };
+ }
+ me.settingGroups = true;
+ // The config system would reject this case as no change
+ // Assume the caller has changed a configuration of the Grouper
+ // and requires the sorting to be redone.
+ if (grouper === data.getGrouper()) {
+ data.updateGrouper(grouper);
+ } else {
+ data.setGrouper(grouper);
+ }
+ delete me.settingGroups;
+ if (change) {
+ if (me.getRemoteSort()) {
+ if (!me.isInitializing) {
+ me.load({
+ scope: me,
+ callback: function() {
+ me.fireGroupChange();
+ }
+ });
+ }
+ } else // do not pass on args
+ {
+ me.fireEvent('datachanged', me);
+ me.fireEvent('refresh', me);
+ me.fireGroupChange();
+ }
+ } else // groupchange event must fire when group is cleared.
+ // The Grouping feature forces a view refresh when changed to a null grouper
+ {
+ me.fireGroupChange();
+ }
+ },
+ fireGroupChange: function(grouper) {
+ var me = this;
+ if (!me.isConfiguring && !me.destroying && !me.destroyed) {
+ me.fireGroupChangeEvent(grouper || me.getGrouper());
+ }
+ },
+ fireGroupChangeEvent: function(grouper) {
+ this.fireEvent('groupchange', this, grouper);
+ },
+ /**
+ * Clear the store grouping
+ */
+ clearGrouping: function() {
+ this.group(null);
+ },
+ getGroupField: function() {
+ var grouper = this.getGrouper(),
+ group = '';
+ if (grouper) {
+ group = grouper.getProperty();
+ }
+ return group;
+ },
+ /**
+ * Tests whether the store currently has an active grouper.
+ * @return {Boolean} `true` if the store is grouped.
+ */
+ isGrouped: function() {
+ return !!this.getGrouper();
+ },
+ applyGrouper: function(grouper) {
+ this.group(grouper);
+ return this.getData().getGrouper();
+ },
+ /**
+ * Returns a collection of readonly sub-collections of your store's records
+ * with grouping applied. These sub-collections are maintained internally by
+ * the collection.
+ *
+ * See {@link #groupField}, {@link #groupDir}. Example for a store
+ * containing records with a color field:
+ *
+ * var myStore = Ext.create('Ext.data.Store', {
+ * groupField: 'color',
+ * groupDir : 'DESC'
+ * });
+ *
+ * myStore.getGroups();
+ *
+ * The above should result in the following format:
+ *
+ * [
+ * {
+ * name: 'yellow',
+ * children: [
+ * // all records where the color field is 'yellow'
+ * ]
+ * },
+ * {
+ * name: 'red',
+ * children: [
+ * // all records where the color field is 'red'
+ * ]
+ * }
+ * ]
+ *
+ * Group contents are affected by filtering.
+ *
+ * @return {Ext.util.Collection} The grouped data
+ */
+ getGroups: function() {
+ return this.getData().getGroups();
+ },
+ onEndUpdate: Ext.emptyFn,
+ privates: {
+ _metaProperties: {
+ count: 'getCount',
+ first: 'first',
+ last: 'last',
+ loading: 'hasPendingLoad',
+ totalCount: 'getTotalCount'
+ },
+ interpret: function(name) {
+ var me = this,
+ accessor = me._metaProperties[name];
+ return accessor && me[accessor]();
+ },
+ // e.g., me.getCount()
+ loadsSynchronously: Ext.privateFn,
+ onRemoteFilterSet: function(filters, remoteFilter) {
+ if (filters) {
+ filters[remoteFilter ? 'on' : 'un']('endupdate', 'onFilterEndUpdate', this);
+ }
+ },
+ // If remoteSort is set, we react to the endUpdate of the sorters Collection by reloading
+ // if there are still some sorters, or we're configured to reload on sorter remove.
+ // If remoteSort is set, we do not need to listen for the data Collection's beforesort
+ // event.
+ //
+ // If local sorting, we do not need to react to the endUpdate of the sorters Collection.
+ // If local sorting, we listen for the data Collection's beforesort event to fire our
+ // beforesort event.
+ onRemoteSortSet: function(sorters, remoteSort) {
+ var me = this,
+ data;
+ if (sorters) {
+ sorters[remoteSort ? 'on' : 'un']('endupdate', 'onSorterEndUpdate', me);
+ data = me.getData();
+ if (data) {
+ data[remoteSort ? 'un' : 'on']('beforesort', 'onBeforeCollectionSort', me);
+ }
+ }
+ }
+ },
+ deprecated: {
+ 5: {
+ methods: {
+ destroyStore: function() {
+ this.destroy();
+ }
+ }
+ }
+ }
+});
+
+/**
+ * This class hols the results of a validator for an `Ext.data.Model`. These objects are
+ * placed in an `Ext.data.ErrorCollection` and returned by `{@link Ext.data.Model#validate}`.
+ *
+ * Usually this class does not need to be instantiated directly - instances are instead created
+ * automatically when {@link Ext.data.Model#validate validate} on a model instance.
+ *
+ * @deprecated 5.0 Use `Ext.data.Validation` instead.
+ */
+Ext.define('Ext.data.Error', {
+ isError: true,
+ $configPrefixed: false,
+ // compat
+ config: {
+ /**
+ * @cfg {String} field
+ * The name of the field this error belongs to.
+ */
+ field: null,
+ /**
+ * @cfg {String} message
+ * The message containing the description of the error.
+ */
+ message: ''
+ },
+ constructor: function(config) {
+ this.initConfig(config);
+ this.msg = this.message;
+ }
+});
+// compat
+
+/**
+ * Wraps a collection of validation error responses and provides convenient functions for
+ * accessing and errors for specific fields.
+ *
+ * Usually this class does not need to be instantiated directly - instances are instead
+ * created automatically when {@link Ext.data.Model#validate validate} on a model instance:
+ *
+ * // Validate some existing model instance - in this case it returned 2 failures
+ * // messages
+ *
+ * var errors = myModel.validate();
+ * errors.isValid(); //false
+ *
+ * errors.length; //2
+ * errors.getByField('name'); // [{field: 'name', message: 'must be present'}]
+ * errors.getByField('title'); // [{field: 'title', message: 'is too short'}]
+ */
+Ext.define('Ext.data.ErrorCollection', {
+ extend: Ext.util.MixedCollection,
+ // not Ext.util.Collection due to API differences
+ alternateClassName: 'Ext.data.Errors',
+ init: function(record) {
+ var me = this,
+ fields = record.fields,
+ data = record.data,
+ before, field, i, len, msg, val, name;
+ for (i = 0 , len = fields.length; i < len; ++i) {
+ field = fields[i];
+ name = field.name;
+ val = data[name];
+ if (field.validate && !field.validate.$nullFn) {
+ before = me.length;
+ msg = field.validate(val, null, me, record);
+ if (before === me.length && msg !== true) {
+ me.add(name, msg);
+ }
+ }
+ }
+ return me;
+ },
+ add: function(key, value) {
+ var me = this,
+ defaultMessage = Ext.data.field.Field.defaultInvalidMessage,
+ obj = key,
+ // for single argument form
+ current;
+ if (Ext.isString(key)) {
+ obj = new Ext.data.Error({
+ field: key,
+ message: value || defaultMessage
+ });
+ } else {
+ if (!(obj.isError)) {
+ obj = new Ext.data.Error({
+ field: obj.field || obj.name,
+ message: obj.error || obj.message || obj.msg || defaultMessage
+ });
+ }
+ key = obj.field;
+ }
+ current = me.get(key);
+ if (current) {
+ if (Ext.isArray(current)) {
+ current.push(obj);
+ return current;
+ }
+ me.removeAtKey(key);
+ obj = [
+ current,
+ obj
+ ];
+ obj.field = key;
+ // Because the value we want in the collection is an array, we need to wrap it
+ // another layer of array or the base add method will add each element.
+ obj = [
+ obj
+ ];
+ }
+ return me.callParent([
+ obj
+ ]);
+ },
+ getKey: function(item) {
+ return item.field;
+ },
+ /**
+ * Returns true if there are no errors in the collection
+ * @return {Boolean}
+ */
+ isValid: function() {
+ return this.length === 0;
+ },
+ /**
+ * Returns all of the errors for the given field
+ * @param {String} fieldName The field to get errors for
+ * @return {Object[]} All errors for the given field
+ */
+ getByField: function(fieldName) {
+ var values = this.get(fieldName);
+ if (values && !Ext.isArray(values)) {
+ values = [
+ values
+ ];
+ }
+ return values || [];
+ }
+});
+
+/**
+ * Represents a read or write operation performed by a {@link Ext.data.proxy.Proxy Proxy}.
+ * Operation objects are used to enable communication between Stores and Proxies.
+ * Application developers should rarely need to interact with Operation objects directly.
+ *
+ * Several Operations can be batched together in a {@link Ext.data.Batch batch}.
+ */
+Ext.define('Ext.data.operation.Operation', {
+ alternateClassName: 'Ext.data.Operation',
+ isOperation: true,
+ config: {
+ /**
+ * @cfg {Boolean} synchronous
+ * True if this Operation is to be executed synchronously. This property is inspected by a
+ * {@link Ext.data.Batch Batch} to see if a series of Operations can be executed in parallel
+ * or not.
+ */
+ synchronous: false,
+ /**
+ * @cfg {String} url
+ * The url for this operation. Typically this will be provided by a proxy and not configured
+ * here.
+ */
+ url: '',
+ /**
+ * @cfg {Object} params
+ * Parameters to pass along with the request when performing the operation.
+ */
+ params: undefined,
+ /**
+ * @cfg {Function} callback
+ * Function to execute when operation completed.
+ * @cfg {Ext.data.Model[]} callback.records Array of records.
+ * @cfg {Ext.data.operation.Operation} callback.operation The Operation itself.
+ * @cfg {Boolean} callback.success True when operation completed successfully.
+ */
+ callback: undefined,
+ /**
+ * @cfg {Object} scope
+ * Scope for the {@link #callback} function.
+ */
+ scope: undefined,
+ /**
+ * @cfg {Ext.data.ResultSet} resultSet
+ * The ResultSet for this operation.
+ * @accessor
+ */
+ resultSet: null,
+ /**
+ * @private
+ * @cfg {Object} response
+ * The response for this operation.
+ */
+ response: null,
+ /**
+ * @cfg {Ext.data.Request} request
+ * The request for this operation.
+ */
+ request: null,
+ /**
+ * @cfg {Ext.data.Model[]} records
+ * The records associated with this operation. If this is a `read` operation, this will be
+ * `null` until data is returned from the {@link Ext.data.proxy.Proxy}.
+ */
+ records: null,
+ /**
+ * @cfg {Object} id
+ * The id of the operation.
+ */
+ id: undefined,
+ /**
+ * @cfg {Ext.data.proxy.Proxy} proxy
+ * The proxy for this operation
+ */
+ proxy: null,
+ /**
+ * @cfg {Ext.data.Batch}
+ * The batch for this operation, if applicable
+ */
+ batch: null,
+ /**
+ * @cfg {Function} recordCreator
+ * Passed to the reader, see {@link Ext.data.reader.Reader#read}
+ * @private
+ */
+ recordCreator: null,
+ // We use this because in a lot of cases the developer can indirectly pass
+ // a callback/scope and that will get pushed on to the operation. As such,
+ // create our own hook for the callback that will fire first
+ /**
+ * @cfg {Function} internalCallback
+ * A callback to run before the {@link #callback}.
+ * @private
+ */
+ internalCallback: null,
+ /**
+ * @cfg {Object} internalScope
+ * Scope to run the {@link #internalCallback}
+ * @private
+ */
+ internalScope: null
+ },
+ /**
+ * @property {Number} order
+ * This number is used by `{@link Ext.data.Batch#sort}` to order operations. Order of
+ * operations of the same type is determined by foreign-key dependencies. The array of
+ * operations is sorted by ascending (increasing) values of `order`.
+ * @private
+ * @readonly
+ * @since 5.0.0
+ */
+ order: 0,
+ /**
+ * @property {Number} foreignKeyDirection
+ * This number is used by `{@link Ext.data.Batch#sort}` to order operations of the
+ * same type. This value is multipled by the "entity rank" (which is determined by
+ * foreign-key dependencies) to invert the direction of the sort based on the type of
+ * operation. Only `Ext.data.operation.Destroy` overrides this value.
+ * @private
+ * @readonly
+ * @since 5.0.0
+ */
+ foreignKeyDirection: 1,
+ /**
+ * @property {Boolean} started
+ * The start status of this Operation. Use {@link #isStarted}.
+ * @readonly
+ * @private
+ */
+ started: false,
+ /**
+ * @property {Boolean} running
+ * The run status of this Operation. Use {@link #isRunning}.
+ * @readonly
+ * @private
+ */
+ running: false,
+ /**
+ * @property {Boolean} complete
+ * The completion status of this Operation. Use {@link #isComplete}.
+ * @readonly
+ * @private
+ */
+ complete: false,
+ /**
+ * @property {Boolean} success
+ * Whether the Operation was successful or not. This starts as undefined and is set to true
+ * or false by the Proxy that is executing the Operation. It is also set to false by
+ * {@link #setException}. Use {@link #wasSuccessful} to query success status.
+ * @readonly
+ * @private
+ */
+ success: undefined,
+ /**
+ * @property {Boolean} exception
+ * The exception status of this Operation. Use {@link #hasException} and see {@link #getError}.
+ * @readonly
+ * @private
+ */
+ exception: false,
+ /**
+ * @property {String/Object} error
+ * The error object passed when {@link #setException} was called. This could be any object or
+ * primitive.
+ * @private
+ */
+ error: undefined,
+ idPrefix: 'ext-operation-',
+ /**
+ * Creates new Operation object.
+ * @param {Object} config (optional) Config object.
+ */
+ constructor: function(config) {
+ // This ugliness is here to prevent an issue when specifying scope
+ // as an object literal. The object will be pulled in as part of
+ // the merge() during initConfig which will change the object
+ // reference. As such, we attempt to fudge it here until we come
+ // up with a better solution for describing how specific config
+ // objects should behave during init() time.
+ var scope = config && config.scope;
+ this.initConfig(config);
+ if (config) {
+ config.scope = scope;
+ }
+ if (scope) {
+ this.setScope(scope);
+ this.initialConfig.scope = scope;
+ }
+ // We need an internal id to track operations in Proxy
+ this._internalId = Ext.id(this, this.idPrefix);
+ },
+ getAction: function() {
+ return this.action;
+ },
+ /**
+ * @private
+ * Executes the operation on the configured {@link #proxy}.
+ * @return {Ext.data.Request} The request object
+ */
+ execute: function() {
+ var me = this,
+ request;
+ delete me.error;
+ delete me.success;
+ me.complete = me.exception = false;
+ me.setStarted();
+ me.request = request = me.doExecute();
+ if (request) {
+ request.setOperation(me);
+ }
+ return request;
+ },
+ doExecute: Ext.emptyFn,
+ /**
+ * Aborts the processing of this operation on the {@link #proxy}.
+ * This is only valid for proxies that make asynchronous requests.
+ */
+ abort: function() {
+ var me = this,
+ request = me.request,
+ proxy;
+ me.aborted = true;
+ if (me.running && request) {
+ proxy = me.getProxy();
+ if (proxy && !proxy.destroyed) {
+ proxy.abort(request);
+ }
+ me.request = null;
+ }
+ me.running = false;
+ },
+ process: function(resultSet, request, response, autoComplete) {
+ var me = this;
+ autoComplete = autoComplete !== false;
+ me.setResponse(response);
+ me.setResultSet(resultSet);
+ if (resultSet.getSuccess()) {
+ me.doProcess(resultSet, request, response);
+ me.setSuccessful(autoComplete);
+ } else if (autoComplete) {
+ me.setException(resultSet.getMessage());
+ }
+ },
+ /**
+ * @private
+ * This private object is used to save on memory allocation. This instance is used to
+ * apply server record updates as part of a record commit performed by calling the
+ * set() method on the record. See doProcess.
+ */
+ _commitSetOptions: {
+ convert: true,
+ commit: true
+ },
+ /**
+ * Process records in the operation after the response is successful and the result
+ * set is parsed correctly. The base class implementation of this method is used by
+ * "create" and "update" operations to allow the server response to update the client
+ * side records.
+ *
+ * @param {Ext.data.ResultSet} resultSet The result set
+ * @param {Ext.data.Request} request The request
+ * @param {Object} response The response
+ * @protected
+ */
+ doProcess: function(resultSet, request, response) {
+ var me = this,
+ commitSetOptions = me._commitSetOptions,
+ clientRecords = me.getRecords(),
+ clientLen = clientRecords.length,
+ clientIdProperty = clientRecords[0].clientIdProperty,
+ serverRecords = resultSet.getRecords(),
+ // a data array, not records yet
+ serverLen = serverRecords ? serverRecords.length : 0,
+ clientMap, serverRecord, clientRecord, i;
+ if (serverLen && clientIdProperty) {
+ // Linear pass over clientRecords to map them by their idProperty
+ clientMap = Ext.Array.toValueMap(clientRecords, 'id');
+ // Linear pass over serverRecords to match them by clientIdProperty to the
+ // corresponding clientRecord (if one exists).
+ for (i = 0; i < serverLen; ++i) {
+ serverRecord = serverRecords[i];
+ clientRecord = clientMap[serverRecord[clientIdProperty]];
+ if (clientRecord) {
+ // Remove this one so we don't commit() on it next
+ delete clientMap[clientRecord.id];
+ // Remove the clientIdProperty value since we don't want to store it
+ delete serverRecord[clientIdProperty];
+ clientRecord.set(serverRecord, commitSetOptions);
+ } else // set & commit
+ {
+ Ext.log.warn('Ignoring server record: ' + Ext.encode(serverRecord));
+ }
+ }
+ // Linear pass over any remaining client records.
+ for (i in clientMap) {
+ clientMap[i].commit();
+ }
+ } else {
+ // Either no serverRecords or no clientIdProperty, so index correspondence is
+ // all we have to go on. If there is no serverRecord at a given index we just
+ // commit() the record.
+ for (i = 0; i < clientLen; ++i) {
+ clientRecord = clientRecords[i];
+ if (serverLen === 0 || !(serverRecord = serverRecords[i])) {
+ // once i > serverLen then serverRecords[i] will be undefined...
+ clientRecord.commit();
+ } else {
+ clientRecord.set(serverRecord, commitSetOptions);
+ }
+ }
+ }
+ },
+ /**
+ * Marks the Operation as started.
+ */
+ setStarted: function() {
+ this.started = this.running = true;
+ },
+ /**
+ * Marks the Operation as completed.
+ */
+ setCompleted: function() {
+ var me = this,
+ proxy;
+ me.complete = true;
+ me.running = false;
+ if (!me.destroying) {
+ me.triggerCallbacks();
+ }
+ // Operation can be destroyed in callback
+ if (me.destroyed) {
+ return;
+ }
+ proxy = me.getProxy();
+ // Store and proxy could be destroyed in callbacks
+ if (proxy && !proxy.destroyed) {
+ proxy.completeOperation(me);
+ }
+ },
+ /**
+ * Marks the Operation as successful.
+ * @param {Boolean} [complete] `true` to also mark this operation
+ * as being complete See {@link #setCompleted}.
+ */
+ setSuccessful: function(complete) {
+ this.success = true;
+ if (complete) {
+ this.setCompleted();
+ }
+ },
+ /**
+ * Marks the Operation as having experienced an exception. Can be supplied with an option
+ * error message/object.
+ * @param {String/Object} error (optional) error string/object
+ */
+ setException: function(error) {
+ var me = this;
+ me.exception = true;
+ me.success = me.running = false;
+ me.error = error;
+ me.setCompleted();
+ },
+ triggerCallbacks: function() {
+ var me = this,
+ callback = me.getInternalCallback();
+ // Call internal callback first (usually the Store's onProxyLoad method)
+ if (callback) {
+ callback.call(me.getInternalScope() || me, me);
+ // Operation callback can cause it to be destroyed
+ if (me.destroyed) {
+ return;
+ }
+ me.setInternalCallback(null);
+ me.setInternalScope(null);
+ }
+ // Call the user's callback as passed to Store's read/write
+ callback = me.getCallback();
+ if (callback) {
+ // Maintain the public API for callback
+ callback.call(me.getScope() || me, me.getRecords(), me, me.wasSuccessful());
+ if (me.destroyed) {
+ return;
+ }
+ me.setCallback(null);
+ me.setScope(null);
+ }
+ },
+ /**
+ * Returns true if this Operation encountered an exception (see also {@link #getError})
+ * @return {Boolean} True if there was an exception
+ */
+ hasException: function() {
+ return this.exception;
+ },
+ /**
+ * Returns the error string or object that was set using {@link #setException}
+ * @return {String/Object} The error object
+ */
+ getError: function() {
+ return this.error;
+ },
+ /**
+ * Returns the {@link Ext.data.Model record}s associated with this operation. For read
+ * operations the records as set by the {@link Ext.data.proxy.Proxy Proxy} will be
+ * returned (returns `null` if the proxy has not yet set the records).
+ *
+ * For create, update, and destroy operations the operation's initially configured
+ * records will be returned, although the proxy may modify these records' data at some
+ * point after the operation is initialized.
+ *
+ * @return {Ext.data.Model[]}
+ */
+ getRecords: function() {
+ var resultSet;
+ /* eslint-disable-next-line no-cond-assign */
+ return this._records || ((resultSet = this.getResultSet()) ? resultSet.getRecords() : null);
+ },
+ /**
+ * Returns true if the Operation has been started. Note that the Operation may have started
+ * AND completed, see {@link #isRunning} to test if the Operation is currently running.
+ * @return {Boolean} True if the Operation has started
+ */
+ isStarted: function() {
+ return this.started;
+ },
+ /**
+ * Returns true if the Operation has been started but has not yet completed.
+ * @return {Boolean} True if the Operation is currently running
+ */
+ isRunning: function() {
+ return this.running;
+ },
+ /**
+ * Returns true if the Operation has been completed
+ * @return {Boolean} True if the Operation is complete
+ */
+ isComplete: function() {
+ return this.complete;
+ },
+ /**
+ * Returns true if the Operation has completed and was successful
+ * @return {Boolean} True if successful
+ */
+ wasSuccessful: function() {
+ return this.isComplete() && this.success === true;
+ },
+ // success can be undefined
+ /**
+ * Checks whether this operation should cause writing to occur.
+ * @return {Boolean} Whether the operation should cause a write to occur.
+ */
+ allowWrite: function() {
+ return true;
+ },
+ destroy: function() {
+ var me = this;
+ me.destroying = true;
+ if (me.running) {
+ me.abort();
+ }
+ // Cleanup upon destruction can be turned off
+ me._params = me._callback = me._scope = me._resultSet = me._response = null;
+ me.request = me._request = me._records = me._proxy = me._batch = null;
+ me._recordCreator = me._internalCallback = me._internalScope = null;
+ me.callParent();
+ }
+});
+
+/**
+ * Encapsulates a create operation as performed by a {@link Ext.data.proxy.Proxy proxy}.
+ *
+ * This class is instantiated by {@link Ext.data.Store stores} and {@link Ext.data.Model records}
+ * and should not need to be instantiated in user code.
+ */
+Ext.define('Ext.data.operation.Create', {
+ extend: Ext.data.operation.Operation,
+ alias: 'data.operation.create',
+ action: 'create',
+ isCreateOperation: true,
+ order: 10,
+ config: {
+ recordCreator: Ext.identityFn
+ },
+ doExecute: function() {
+ return this.getProxy().create(this);
+ }
+});
+
+/**
+ * Encapsulates a destroy operation as performed by a {@link Ext.data.proxy.Proxy proxy}.
+ *
+ * This class is instantiated by {@link Ext.data.Store stores} and {@link Ext.data.Model records}
+ * and should not need to be instantiated in user code.
+ */
+Ext.define('Ext.data.operation.Destroy', {
+ extend: Ext.data.operation.Operation,
+ alias: 'data.operation.destroy',
+ action: 'destroy',
+ isDestroyOperation: true,
+ order: 30,
+ foreignKeyDirection: -1,
+ doProcess: function() /* resultSet, request, response */
+ {
+ var clientRecords = this.getRecords(),
+ clientLen = clientRecords.length,
+ i;
+ for (i = 0; i < clientLen; ++i) {
+ clientRecords[i].setErased();
+ }
+ },
+ doExecute: function() {
+ return this.getProxy().erase(this);
+ },
+ getRecordData: function(record, operation) {
+ var data = {},
+ idField = record.idField,
+ nameProperty = this.getNameProperty() || 'name';
+ data[idField[nameProperty]] = record.id;
+ return data;
+ }
+});
+
+/**
+ * Encapsulates a read operation as performed by a {@link Ext.data.proxy.Proxy proxy}.
+ *
+ * This class is instantiated by {@link Ext.data.Store stores} and {@link Ext.data.Model records}
+ * and should not need to be instantiated in user code.
+ */
+Ext.define('Ext.data.operation.Read', {
+ extend: Ext.data.operation.Operation,
+ alias: 'data.operation.read',
+ action: 'read',
+ isReadOperation: true,
+ config: {
+ /**
+ * @cfg {Ext.util.Filter[]} filters
+ * Optional array of filter objects. Only applies to 'read' actions.
+ */
+ filters: undefined,
+ /**
+ * @cfg {Ext.util.Sorter[]} sorters
+ * Optional array of sorter objects. Only applies to 'read' actions.
+ */
+ sorters: undefined,
+ /**
+ * @cfg {Ext.util.Grouper} grouper
+ * Optional grouping configuration. Only applies to 'read' actions where grouping is
+ * desired.
+ */
+ grouper: undefined,
+ /**
+ * @cfg {Number} start
+ * The start index (offset), used in paging when running a 'read' action.
+ */
+ start: undefined,
+ /**
+ * @cfg {Number} limit
+ * The number of records to load. Used on 'read' actions when paging is being used.
+ */
+ limit: undefined,
+ /**
+ * @cfg {Number} page
+ * The page for this operation.
+ */
+ page: undefined,
+ /**
+ * @cfg {Boolean} addRecords
+ * Passed internally to loadRecords when the load completes
+ * @private
+ */
+ addRecords: false
+ },
+ doExecute: function() {
+ return this.getProxy().read(this);
+ },
+ doProcess: Ext.emptyFn,
+ allowWrite: function() {
+ return false;
+ }
+});
+
+/**
+ * Encapsulates a update operation as performed by a {@link Ext.data.proxy.Proxy proxy}.
+ *
+ * This class is instantiated by {@link Ext.data.Store stores} and {@link Ext.data.Model records}
+ * and should not need to be instantiated in user code.
+ */
+Ext.define('Ext.data.operation.Update', {
+ extend: Ext.data.operation.Operation,
+ alias: 'data.operation.update',
+ action: 'update',
+ isUpdateOperation: true,
+ order: 20,
+ config: {
+ recordCreator: Ext.identityFn
+ },
+ doExecute: function() {
+ return this.getProxy().update(this);
+ }
+});
+
+/**
+ * This class defines a series of static methods that are used on a
+ * {@link Ext.data.Field} for performing sorting. The methods cast the
+ * underlying values into a data type that is appropriate for sorting on
+ * that particular field. If a {@link Ext.data.Field#type} is specified,
+ * the sortType will be set to a sane default if the sortType is not
+ * explicitly defined on the field. The sortType will make any necessary
+ * modifications to the value and return it.
+ *
+ * - **`asText`** - Removes any tags and converts the value to a string
+ * - **`asUCText`** - Removes any tags and converts the value to an uppercase string
+ * - **`asUCString`** - Converts the value to an uppercase string
+ * - **`asDate`** - Converts the value into Unix epoch time
+ * - **`asFloat`** - Converts the value to a floating point number
+ * - **`asInt`** - Converts the value to an integer number
+ *
+ * It is also possible to create a custom sortType that can be used throughout
+ * an application.
+ *
+ * Ext.apply(Ext.data.SortTypes, {
+ * asPerson: function(person){
+ * // expects an object with a first and last name property
+ * return person.lastName.toUpperCase() + person.firstName.toLowerCase();
+ * }
+ * });
+ *
+ * Ext.define('Employee', {
+ * extend: 'Ext.data.Model',
+ * fields: [{
+ * name: 'person',
+ * sortType: 'asPerson'
+ * }, {
+ * name: 'salary',
+ * type: 'float' // sortType set to asFloat
+ * }]
+ * });
+ *
+ * @singleton
+ */
+Ext.define('Ext.data.SortTypes', function() {
+ var me;
+ return {
+ singleton: true,
+ constructor: function() {
+ me = this;
+ },
+ /**
+ * Default sort that does nothing
+ * @param {Object} s The value being converted
+ * @return {Object} The comparison value
+ */
+ none: Ext.identityFn,
+ /**
+ * The regular expression used to strip commas
+ * @type {RegExp}
+ * @property
+ */
+ stripCommasRe: /,/g,
+ /**
+ * The regular expression used to strip tags
+ * @type {RegExp}
+ * @property
+ */
+ stripTagsRE: /<\/?[^>]+>/gi,
+ /**
+ * Strips all HTML tags to sort on text only
+ * @param {Object} s The value being converted
+ * @return {String} The comparison value
+ */
+ asText: function(s) {
+ // If allowNull, return the Unicode null character.
+ return (s != null) ? String(s).replace(me.stripTagsRE, '') : '\x00';
+ },
+ /**
+ * Strips all HTML tags to sort on text only - Case insensitive
+ * @param {Object} s The value being converted
+ * @return {String} The comparison value
+ */
+ asUCText: function(s) {
+ // If allowNull, return the Unicode null character.
+ /* eslint-disable-next-line newline-per-chained-call */
+ return (s != null) ? String(s).toUpperCase().replace(me.stripTagsRE, '') : '\x00';
+ },
+ /**
+ * Case insensitive string
+ * @param {Object} s The value being converted
+ * @return {String} The comparison value
+ */
+ asUCString: function(s) {
+ // If allowNull, return the Unicode null character.
+ return (s != null) ? String(s).toUpperCase() : '\x00';
+ },
+ /**
+ * Date sorting
+ * @param {Object} s The value being converted
+ * @return {Number} The comparison value
+ */
+ asDate: function(s) {
+ if (!s) {
+ return 0;
+ }
+ if (Ext.isDate(s)) {
+ return s.getTime();
+ }
+ return Date.parse(String(s));
+ },
+ /**
+ * Float sorting
+ * @param {Object} s The value being converted
+ * @return {Number} The comparison value
+ */
+ asFloat: function(s) {
+ var val = parseFloat(String(s).replace(me.stripCommasRe, ''));
+ return isNaN(val) ? 0 : val;
+ },
+ /**
+ * Integer sorting
+ * @param {Object} s The value being converted
+ * @return {Number} The comparison value
+ */
+ asInt: function(s) {
+ var val = parseInt(String(s).replace(me.stripCommasRe, ''), 10);
+ return isNaN(val) ? 0 : val;
+ }
+ };
+});
+
+/**
+ * The base class for validators to be used to validate {@link Ext.data.Field fields} in
+ * a {@link Ext.data.Model model}.
+ *
+ * The model will call the {@link #validate} method, which may be overridden by subclasses.
+ */
+Ext.define('Ext.data.validator.Validator', {
+ mixins: [
+ Ext.mixin.Factoryable
+ ],
+ alias: 'data.validator.base',
+ // also configures Factoryable
+ isValidator: true,
+ factoryConfig: {
+ cacheable: true
+ },
+ /**
+ * @property {String} type
+ * A string representation of this format.
+ */
+ type: 'base',
+ /**
+ * Creates new Validator.
+ * @param {Object/Function} config A config object. A function may also be passed,
+ * which will be used as the {@link #validate} method for this validator.
+ */
+ constructor: function(config) {
+ if (typeof config === 'function') {
+ this.fnOnly = true;
+ this.validate = config;
+ } else {
+ this.initConfig(config);
+ }
+ },
+ /**
+ * Validates the passed value.
+ * @param {Object} value The value
+ * @param {Ext.data.Model} record The record
+ * @return {Boolean/String} `true` if the value is valid. A string may be returned if the value
+ * is not valid, to indicate an error message. Any other non `true` value indicates the value
+ * is not valid.
+ */
+ validate: function() {
+ return true;
+ },
+ /**
+ * Creates a copy of this validator
+ * @private
+ * @return {Ext.data.validator.Validator} The clone
+ */
+ clone: function() {
+ var me = this;
+ if (me.fnOnly) {
+ return new Ext.data.validator.Validator(me.validate);
+ }
+ return new me.self(me.getCurrentConfig());
+ }
+}, function(Validator) {
+ Ext.Factory.validator = Ext.Factory.dataValidator;
+});
+
+/**
+ * Fields are used to define the members of a Model. They aren't instantiated directly;
+ * instead, when we create a class that extends {@link Ext.data.Model}, it automatically
+ * creates Field instances for each field configured in a {@link Ext.data.Model Model}.
+ * For example, we might set up a model like this:
+ *
+ * Ext.define('User', {
+ * extend: 'Ext.data.Model',
+ * fields: [
+ * 'name', 'email',
+ * { name: 'age', type: 'int' },
+ * { name: 'gender', type: 'string', defaultValue: 'Unknown' }
+ * ]
+ * });
+ *
+ * Four fields will have been created for the User Model - name, email, age and gender.
+ * Note that we specified a couple of different formats here; if we only pass in the string
+ * name of the field (as with name and email), the field is set up with the 'auto' type.
+ * It's as if we'd done this instead:
+ *
+ * Ext.define('User', {
+ * extend: 'Ext.data.Model',
+ * fields: [
+ * { name: 'name', type: 'auto' },
+ * { name: 'email', type: 'auto' },
+ * { name: 'age', type: 'int' },
+ * { name: 'gender', type: 'string', defaultValue: 'Unknown' }
+ * ]
+ * });
+ *
+ * # Field Types
+ *
+ * Fields come in various types. When declaring a field, the `type` property is used to
+ * specify the type of `Field` derived class used to manage values.
+ *
+ * The predefined set of types are:
+ *
+ * - {@link Ext.data.field.Field auto} (Default, implies no conversion)
+ * - {@link Ext.data.field.String string}
+ * - {@link Ext.data.field.Integer int}
+ * - {@link Ext.data.field.Number number}
+ * - {@link Ext.data.field.Boolean boolean}
+ * - {@link Ext.data.field.Date date}
+ *
+ * # Conversion
+ *
+ * When reading fields it is often necessary to convert the values received before using
+ * them or storing them in records. To handle these cases there is the
+ * `{@link #method-convert convert}` method. This method is passed the received value (as
+ * well as the current record instance, but see below) and it returns the value to carry
+ * forward.
+ *
+ * For `auto` fields there is no `{@link #method-convert convert}` method. This is for
+ * efficiency. For other field types, there are often `convert` methods. You can provide
+ * a `{@link #cfg-convert convert}` config when the field is defined like this:
+ *
+ * {
+ * name: 'timestamp',
+ *
+ * convert: function (value) {
+ * return new Date(value);
+ * }
+ * }
+ *
+ * While this can be convenient, see below for details on defining Custom Types as that is
+ * often a better practice and avoids repeating these functions.
+ *
+ * Note that when a `defaultValue` is specified, it will also be passed through to
+ * `convert` (either to the `{@link #method-convert convert}` method or to the
+ * `{@link #cfg-convert convert} config)`.
+ *
+ * ## Calculated Values
+ *
+ * In some cases fields are the result of a calculation from other fields. Historically
+ * this was a second role for `{@link #method-convert convert}` but that has some short
+ * comings. The simpler solution is the `{@link #cfg-calculate calculate}` config.
+ *
+ * Values produced by `{@link #cfg-calculate calculate}` and `{@link #method-convert convert}`
+ * are stored in the record as with any other field. In fact, if we define a calculated
+ * "firstName" field and log out all of the data, we'll see this:
+ *
+ * var ed = Ext.create('User', { name: 'Ed Spencer' });
+ *
+ * console.log(ed.data);
+ *
+ * //outputs this:
+ * {
+ * age: 0,
+ * email: "",
+ * firstName: "Ed", // calculated field
+ * gender: "Unknown",
+ * name: "Ed Spencer"
+ * }
+ *
+ * ### Using `calculate`
+ *
+ * {
+ * name: 'firstName',
+ *
+ * calculate: function (data) {
+ * return data.name.split(' ')[0];
+ * }
+ * }
+ *
+ * Using `{@link #cfg-calculate calculate}` is the simplest and safest way to define a
+ * calculated field. The most important part of this is that, internally, the code of the
+ * supplied function is parsed to extract its dependencies. In this case, the "name" field
+ * is the only dependency. This means that "firstName" will only need to be recalculated
+ * when "name" is modified.
+ *
+ * **Note:** Fields used by the calculate method must be explicitly defined in the
+ * {@link Ext.data.Model#cfg-fields #fields} of the model.
+ *
+ * ### Using `convert`
+ *
+ * Following is the equivalent technique using `{@link #cfg-convert convert}`
+ *
+ * {
+ * name: 'firstName',
+ *
+ * convert: function (value, record) {
+ * return record.get('name').split(' ')[0];
+ * },
+ *
+ * depends: [ 'name' ]
+ * }
+ *
+ * When a `{@link #method-convert convert}` function accepts a 2nd argument (a reference to
+ * the record), it is considered a calculated field. If a `{@link #cfg-depends depends}`
+ * config is not provided then this field's dependencies are unknown. In this case, the
+ * `{@link #cfg-depends depends}` are provided as would be automatically determined with
+ * the `{@link #cfg-calculate calculate}` config.
+ *
+ * ### Updating
+ *
+ * Fields modified with the {@link Ext.data.Model#set set} method will have their stored
+ * value set using the convert / calculate method when present.
+ *
+ * For example:
+ *
+ * Ext.define('MyApp.model.Employee', {
+ * extend: 'Ext.data.Model',
+ * fields: [{
+ * name: 'salary',
+ * convert: function (val) {
+ * var startingBonus = val * .1;
+ * return val + startingBonus;
+ * }
+ * }],
+ * convertOnSet: false
+ * });
+ *
+ * var tina = Ext.create('MyApp.model.Employee', {
+ * salary: 50000
+ * });
+ *
+ * console.log(tina.get('salary')); // logs 55000
+ *
+ * tina.set('salary', 60000);
+ * console.log(tina.get('salary')); // logs 60000
+ *
+ * This default behavior can be disabled by setting the Model's
+ * `{@link Ext.data.Model#cfg-convertOnSet}` config to `false`.
+ *
+ * **Note:** convertOnSet `false` only prevents the convert / calculate call when the
+ * set `fieldName` param matches the field's `{@link #name}`. See
+ * {@link Ext.data.Model#convertOnSet convertOnSet} for additional details.
+ *
+ * ### Dependencies
+ *
+ * When a field's `{@link #method-convert convert}` method processes values from the record
+ * (vs. just the field's value), it is best to also provide a `depends` config as shown
+ * above. Fields that provide a `{@link #cfg-calculate calculate}` method must follow the
+ * proper form for using fields so that dependencies can be extracted.
+ *
+ * Calculated fields are processed after other fields based on their dependencies. Fields
+ * with `{@link #method-convert convert}` methods that use the provided record that do *not*
+ * specify a `{@link #cfg-depends depends}` config are processed as a group after all other
+ * fields since such converters can rely on anything in the record. The order of processing
+ * these fields with respect to each other is unspecified and should not be relied upon.
+ *
+ * # Serialization
+ *
+ * To handle the inverse scenario of `convert` there is the `serialize` method. This
+ * method is called to produce the value to send to a server based on the internal value
+ * as would be returned from `convert`. In most cases, these methods should "round trip"
+ * a value:
+ *
+ * assertEqual(value, field.serialize(field.convert(value)));
+ *
+ * By default, only `{@link Ext.data.field.Date date}` fields have a `serialize` method.
+ * Other types simply send their value unmodified.
+ *
+ * # Custom Types
+ *
+ * Developers may create their own application-specific data types by deriving from this
+ * class. This is typically much better than applying multiple configuration values on
+ * field instances as these often become repetitive.
+ *
+ * To illustrate, we define a "time" field type that stores a time-of-day represented as a
+ * number of minutes since Midnight.
+ *
+ * Ext.define('App.field.Time', {
+ * extend: 'Ext.data.field.Field',
+ *
+ * alias: 'data.field.time',
+ *
+ * timeFormat: 'g:i',
+ *
+ * convert: function (value) {
+ * if (value && Ext.isString(value)) {
+ * var date = Ext.Date.parse(value, this.timeFormat);
+ * if (!date) {
+ * return null;
+ * }
+ * return (date.getHours() - 1) * 60 + date.getMinutes();
+ * }
+ * return value;
+ * }
+ * });
+ *
+ * ## Validation
+ *
+ * Custom field types can override the `{@link #method-validate validate}` method or
+ * provide a set of `{@link #cfg-validators validators}`.
+ *
+ * Ext.define('App.field.PhoneNumber', {
+ * extend: 'Ext.data.field.Field',
+ *
+ * alias: 'data.field.phonenumber',
+ *
+ * // Match U.S. phone numbers for example purposes
+ * validators: {
+ * type: 'format',
+ * matcher: /\d{3}\-\d{3}\-\d{4}/
+ * }
+ * });
+ *
+ * Once the class is defined, fields can be declared using the new type (based on its
+ * `alias`) like so:
+ *
+ * Ext.define('App.model.PhoneCall', {
+ * fields: [
+ * { name: 'startTime', type: 'time' },
+ * { name: 'phoneNumber', type: 'phonenumber' }
+ * ]
+ * });
+ */
+Ext.define('Ext.data.field.Field', {
+ mixins: [
+ Ext.mixin.Factoryable
+ ],
+ alternateClassName: 'Ext.data.Field',
+ alias: 'data.field.auto',
+ // also configures Factoryable
+ aliasPrefix: 'data.field.',
+ type: 'auto',
+ /**
+ * @property factoryConfig
+ * @inheritdoc
+ */
+ factoryConfig: {
+ defaultProperty: 'name'
+ },
+ isDataField: true,
+ isField: true,
+ // NOTE: We do not use "config: {}" here because these configs are simple, never really
+ // set after creation and expensive enough when processed per-instance that avoiding
+ // the overhead is worth while. Remember that a large app may have many dozens of
+ // entities in their data model and these may have many fields each. Easily hundreds
+ // of Field instances. Using config with inherited things (like convert methods) just
+ // pushes the set to the constructor where it needs to just be a normal method.
+ /**
+ * @cfg {Boolean} allowBlank
+ * @private
+ *
+ * Used for validating a {@link Ext.data.Model model}. Defaults to true. An empty value here
+ * will cause {@link Ext.data.Model}.{@link Ext.data.Model#isValid isValid} to evaluate to
+ * `false`.
+ */
+ allowBlank: true,
+ /**
+ * @cfg {Boolean} allowNull
+ *
+ * Use when converting received data into a {@link Ext.data.field.Integer `int`},
+ * {@link Ext.data.field.Number `float`}, {@link Ext.data.field.Boolean `bool`}
+ * or {@link Ext.data.field.String `string`} type. If the value cannot be
+ * parsed, `null` will be used if allowNull is true, otherwise a default value for
+ * that type will be used:
+ *
+ * - for `int` and `float` - `0`.
+ * - for `string` - `""`.
+ * - for `bool` - `false`.
+ *
+ * Note that when parsing of {@link Ext.data.field.Date `date`} type fails, the value
+ * will be `null` regardless of this setting.
+ */
+ allowNull: false,
+ /**
+ * @cfg {Function} calculate
+ * This config defines a simple field calculation function. A calculate method only
+ * has access to the record data and should return the value of the calculated field.
+ * When provided in this way, the `depends` config is automatically determined by
+ * parsing the `calculate` function. For example:
+ *
+ * fields: [{
+ * name: 'firstName',
+ * type: 'string'
+ * },{
+ * name: 'lastName',
+ * type: 'string'
+ * },{
+ * name: 'fullName',
+ * calculate: function (data) {
+ * return data.firstName + ' ' + data.lastName;
+ * }
+ * }]
+ *
+ * The above 'fullName' field is equivalent to:
+ *
+ * {
+ * name: 'fullName',
+ * convert: function (v, rec) {
+ * return rec.get('firstName') + ' ' + rec.get('lastName');
+ * },
+ * depends: ['firstName', 'lastName']
+ * }
+ *
+ * The restrictions on form for a `calculate` method are that the accesses to field
+ * values must match the following regular expression (case insensitive):
+ *
+ * data.([a-z_][a-z0-9_]*)
+ * // where 'data' is the param passed to the calculate method
+ *
+ * The only advantage of a `calculate` method over a `convert` method is automatic
+ * determination of `depends`.
+ *
+ * **Note:** The use of calculate and {@link #method-convert} are exclusive. The
+ * calculate method will override the convert method if both are configured.
+ *
+ * **Note:** Fields used by the calculate method must be explicitly defined in the
+ * {@link Ext.data.Model#cfg-fields #fields} of the model.
+ *
+ * @param {Object} data An object with all values for each field in the parent
+ * model. See {@link Ext.data.Model#getData getData}.
+ * @return {Mixed} value The value of the calculated field
+ */
+ /**
+ * @cfg {Function} convert
+ * If specified this config overrides the `{@link #method-convert convert}` method.
+ * See also `{@link #cfg-calculate calculate}` for simple field calculations.
+ *
+ * **Note:** The use of {@link #calculate} and convert are exclusive. The calculate
+ * method will override the convert method if both are configured.
+ */
+ /**
+ * @cfg {Boolean} critical
+ * A critical field is a field that must always be sent to the server even if it has
+ * not changed. The most common example of such a field is the "id" of a record (see
+ * `{@link Ext.data.Model#idProperty}` but the
+ * `{@link Ext.data.Model#versionProperty}` is similarly a `critical` field.
+ */
+ critical: false,
+ /**
+ * @property {String} defaultInvalidMessage
+ * The default message to present for an invalid field.
+ * @since 5.0.0
+ */
+ defaultInvalidMessage: 'This field is invalid',
+ /**
+ * @cfg {Object} defaultValue
+ *
+ * The default value used when the creating an instance from a raw data object,
+ * and the property referenced by the `{@link Ext.data.field.Field#mapping mapping}`
+ * does not exist in that data object.
+ *
+ * The value `undefined` prevents defaulting in a value.
+ */
+ defaultValue: undefined,
+ /**
+ * @property {Ext.Class} definedBy
+ * The class (derived from {@link Ext.data.Model}) that defined this field.
+ *
+ * Ext.define('MyApp.models.Foo', {
+ * extend: 'Ext.data.Model',
+ *
+ * fields: [
+ * { name: 'bar' }
+ * ],
+ * ...
+ * });
+ *
+ * var barField = MyApp.models.Foo.getField('bar');
+ *
+ * alert(barField.definedBy === MyApp.models.Foo); // alerts 'true'
+ *
+ * When a field is inherited, this value will reference the class that originally
+ * defined the field.
+ *
+ * Ext.define('MyApp.models.Base', {
+ * extend: 'Ext.data.Model',
+ *
+ * fields: [
+ * { name: 'foo' }
+ * ],
+ * ...
+ * });
+ *
+ * Ext.define('MyApp.models.Derived', {
+ * extend: 'MyApp.models.Base',
+ *
+ * fields: [
+ * { name: 'bar' }
+ * ],
+ * ...
+ * });
+ *
+ * var fooField = MyApp.models.Derived.getField('foo');
+ *
+ * alert(fooField.definedBy === MyApp.models.Base); // alerts 'true'
+ */
+ definedBy: null,
+ /**
+ * @cfg {String/String[]} depends
+ * The field name or names within the {@link Ext.data.Model Model} on which the value
+ * of this field depends, and from which a new value may be calculated. These values
+ * are the values used by the `convert` method. If you do not have a `convert` method
+ * then this config should not be specified.
+ *
+ * Before using this config you should consider if using a `calculate` method instead
+ * of a `convert` method would be simpler.
+ *
+ * Whenever any of the named fields are set using the {@link Ext.data.Model#set set}
+ * method, this fields will have its `convert` method called passing the
+ * {@link Ext.data.Model record} so that the dependent value can be calculated from
+ * all fields which it needs.
+ *
+ * For example, to display a person's full name, using two separate `firstName` and
+ * `lastName` fields, configure the name field like this:
+ *
+ * {
+ * name: 'name',
+ *
+ * // Will be called whenever forename or surname fields are set
+ * convert: function (v, rec) {
+ * return rec.get('firstName') + ' ' + rec.get('lastName');
+ * },
+ *
+ * depends: [ 'firstName', 'lastName' ],
+ *
+ * // It should not be returned to the server - it's not a database field
+ * persist: false
+ * }
+ *
+ * Note that if you do not want the calculated field to be part of the field set sent
+ * back to the server when the store is synchronized, you should configure the field
+ * with `persist` set to `false`.
+ */
+ depends: null,
+ /**
+ * @property {Ext.data.field.Field[]} dependents
+ * This array tracks the fields that have indicated this field in their `depends`
+ * list. If no fields depend on this field, this will be `null`.
+ * @readonly
+ * @private
+ */
+ dependents: null,
+ /**
+ * @cfg {String/Number/Function} mapping
+ *
+ * (Optional) A path expression for use by the {@link Ext.data.reader.Reader}
+ * implementation that is creating the {@link Ext.data.Model Model} to extract the
+ * Field value from the data object. If the path expression is the same as the field
+ * name, the mapping may be omitted. A function may be passed to do complex data
+ * extraction. The examples below are simple just to demonstrate the capability,
+ * typically, a function would not be used to extract such simple data.
+ *
+ * The form of the mapping expression depends on the Reader being used.
+ *
+ * - {@link Ext.data.reader.Json}
+ *
+ * The mapping is a string containing the javascript expression to reference the
+ * data from an element of the data item's
+ * {@link Ext.data.reader.Json#cfg-rootProperty rootProperty} Array. Defaults to
+ * the field name. If a function is passed, a single argument is received which
+ * contains the raw json object:
+ *
+ * // Server returns [{"name": "Foo", "age": 1}, {"name": "Bar", "age": 2}]
+ * mapping: function(data) {
+ * return data.name;
+ * }
+ *
+ * - {@link Ext.data.reader.Xml}
+ *
+ * The mapping is an {@link Ext.DomQuery} path to the data item relative to the DOM
+ * element that represents the {@link Ext.data.reader.Xml#record record}. Defaults
+ * to the field name. If a function is passed, a single argument is received which
+ * contains the record node:
+ *
+ * // Server returns:
+ * //
+ * //
+ * // Foo
+ * // 1
+ * //
+ * //
+ * // Bar
+ * // 2
+ * //
+ * //
+ * mapping: function(data) {
+ * return data.firstChild.textContent;
+ * }
+ *
+ * - {@link Ext.data.reader.Array}
+ *
+ * The mapping is a number indicating the Array index of the field's value.
+ * Defaults to the field specification's Array position. If a function is passed, a
+ * single argument is received which contains the child array.
+ *
+ * // Server returns [["Foo", 1], ["Bar", 2]]
+ * mapping: function(data) {
+ * return data[0];
+ * }
+ *
+ * If a more complex value extraction strategy is required, then configure the Field
+ * with a {@link #cfg-convert} function. This is passed the whole row object, and may
+ * interrogate it in whatever way is necessary in order to return the desired data.
+ */
+ mapping: null,
+ /**
+ * @cfg {String} name
+ *
+ * The name by which the field is referenced within the Model. This is referenced by,
+ * for example, the `dataIndex` property in column definition objects passed to
+ * {@link Ext.grid.property.HeaderContainer}.
+ *
+ * Note: In the simplest case, if no properties other than `name` are required, a
+ * field definition may consist of just a String for the field name.
+ */
+ name: null,
+ /**
+ * @property {Number} ordinal
+ *
+ * The position of this field in the {@link Ext.data.Model} in which it was defined.
+ */
+ ordinal: undefined,
+ /**
+ * @cfg {Boolean} persist
+ *
+ * False to exclude this field from the {@link Ext.data.Model#modified} fields in a
+ * record. This will also exclude the field from being written using a
+ * {@link Ext.data.writer.Writer}. This option is useful when fields are used to keep
+ * state on the client but do not need to be persisted to the server.
+ *
+ * Defaults to `false` for `calculated` fields and `true` otherwise.
+ */
+ persist: null,
+ /**
+ * @cfg {String/Object} reference
+ * The {@link Ext.data.Model#entityName name} of the entity referenced by this field.
+ * In most databases, this relationship is represented by a "foreign key". That is, a
+ * value for such a field matches the value of the
+ * {@link Ext.data.Model#idProperty id} for an entity of this type.
+ *
+ * For further documentation, see {@link Ext.data.schema.Reference}.
+ */
+ reference: null,
+ /**
+ * @cfg serialize
+ * @inheritdoc Ext.data.field.Field#method-serialize
+ */
+ /**
+ * @cfg {String/Object/Function} summary
+ * The summary type for this field. This is used to calculate a
+ * summary value by the {@link Ext.data.Model Model}.
+ *
+ * - If a string, it should be an alias for one of the Ext.data.summary types.
+ * - If an object, a config for one of the Ext.data.summary types.
+ * - If a function, it should match the signature for
+ * {@link Ext.data.summary.Base#method-calculate calculate}.
+ *
+ * @since 6.5.0
+ */
+ summary: null,
+ /**
+ * @cfg {String} summaryField
+ * A field to use as the basis for calculating a summary. This is used in
+ * conjunction with the virtual summary fields. See
+ * {@link Ext.data.Model#cfg-summary}.
+ *
+ * @since 6.5.0
+ * @private
+ */
+ summaryField: '',
+ /**
+ * @cfg {Function/String} sortType
+ *
+ * A function which converts a Field's value to a comparable value in order to ensure
+ * correct sort ordering.
+ *
+ * Predefined functions are provided in {@link Ext.data.SortTypes}. A custom sort
+ * example:
+ *
+ * // current sort after sort we want
+ * // +-+------+ +-+------+
+ * // |1|First | |1|First |
+ * // |2|Last | |3|Second|
+ * // |3|Second| |2|Last |
+ * // +-+------+ +-+------+
+ *
+ * sortType: function(value) {
+ * switch (value.toLowerCase()) // native toLowerCase():
+ * {
+ * case 'first': return 1;
+ * case 'second': return 2;
+ * default: return 3;
+ * }
+ * }
+ *
+ * May also be set to a String value, corresponding to one of the named sort types in
+ * {@link Ext.data.SortTypes}.
+ */
+ /**
+ * @cfg {Boolean} [unique=false]
+ * `true` if the value of this field is unique amongst all instances. When used with a
+ * `reference` this describes a "one-to-one" relationship. It is almost always the
+ * case that a `unique` field cannot also be {@link #allowBlank nullable}.
+ */
+ unique: false,
+ /**
+ * @cfg {Object[]} validators
+ * An array of {@link Ext.data.validator.Validator validators} for this field. These
+ * `validators` will only be passed a field value to validate.
+ */
+ /**
+ * @property {Number} rank
+ * This is a 1-based value that describes the dependency order of this field. This is
+ * initialized to `null` (falsy) so we can cheaply topo-sort the fields of a class.
+ * @private
+ * @readonly
+ */
+ rank: null,
+ /**
+ * @property {RegExp} stripRe
+ * A regular expression for stripping non-numeric characters from a numeric value.
+ * This should be overridden for localization.
+ * @readonly
+ * @protected
+ */
+ stripRe: /[$,%]/g,
+ /**
+ * @property {Boolean} calculated
+ * This property is `true` if this field has a `{@link #cfg-calculate calculate}`
+ * method or a `{@link #method-convert convert}` method that operates on the entire
+ * record as opposed to just the data value. This property is determined from the
+ * `length` of the `{@link #method-convert convert}` function which means this is
+ * *not* calculated:
+ *
+ * convert: function (value) {
+ * return ...
+ * }
+ *
+ * While this *is* calculated:
+ *
+ * convert: function (value, record) {
+ * return ...
+ * }
+ *
+ * **NOTE:** It is recommended for such fields to use
+ * `{@link #cfg-calculate calculate}` or explicitly specify the fields used by
+ * `{@link #method-convert convert}` using `{@link #cfg-depends depends}`.
+ *
+ * @readonly
+ */
+ calculated: false,
+ /**
+ * @property {Boolean} evil
+ * This flag is set to true for fields that have `convert` methods which take the 2nd
+ * argument (the record) and do not specify a `depends` set. Good fields indicate the
+ * fields on which they depend (if any).
+ * @private
+ * @readonly
+ */
+ evil: false,
+ /**
+ * @property {Boolean} identifier
+ * This property is set to `true` if this is an {@link Ext.data.Model#idProperty id}
+ * field.
+ * @readonly
+ */
+ identifier: false,
+ onClassExtended: function(cls, data) {
+ var sortType = data.sortType,
+ proto = cls.prototype,
+ superValidators = proto.validators,
+ validators = data.validators,
+ alias = data.alias;
+ if (alias) {
+ if (typeof alias !== 'string') {
+ alias = alias[0];
+ }
+ if (alias) {
+ proto.type = alias.substr(alias.lastIndexOf('.') + 1);
+ }
+ }
+ if (sortType && Ext.isString(sortType)) {
+ proto.sortType = Ext.data.SortTypes[sortType];
+ }
+ if (validators) {
+ // Force validators to be an array
+ if (!Ext.isArray(validators)) {
+ validators = [
+ validators
+ ];
+ }
+ delete data.validators;
+ // Need to join them
+ if (superValidators) {
+ validators = superValidators.concat(validators);
+ }
+ proto.validators = validators;
+ }
+ },
+ argumentNamesRe: /^function\s*\(\s*([^,\)\s]+)/,
+ // eslint-disable-line no-useless-escape
+ calculateRe: /[^.a-z0-9_]([a-z_][a-z_0-9]*)\.([a-z_][a-z_0-9]*)/gi,
+ constructor: function(config) {
+ var me = this,
+ calculateRe = me.calculateRe,
+ calculate, calculated, defaultValue, sortType, depends, map, match, dataProp, str, fld, validators;
+ // NOTE: In bigger apps we create *lots* of these fellows so we really need to be
+ // very lean here.
+ if (config) {
+ if (Ext.isString(config)) {
+ me.name = config;
+ } else {
+ validators = config.validators;
+ if (validators) {
+ delete config.validators;
+ me.instanceValidators = validators;
+ }
+ Ext.apply(me, config);
+ }
+ }
+ if (!me.allowNull) {
+ me.allowNull = !!me.reference;
+ }
+ calculate = me.calculate;
+ depends = me.depends;
+ if (calculate) {
+ me.convert = me.doCalculate;
+ if (!depends) {
+ if (!(depends = calculate.$depends)) {
+ map = {};
+ str = Ext.Function.toCode(calculate);
+ // strips comments in debug
+ calculate.$depends = depends = [];
+ match = me.argumentNamesRe.exec(str);
+ dataProp = match ? match[1] : 'data';
+ while ((match = calculateRe.exec(str))) {
+ if (dataProp === match[1] && !map[fld = match[2]]) {
+ map[fld] = 1;
+ depends.push(fld);
+ }
+ }
+ }
+ me.depends = depends;
+ }
+ }
+ defaultValue = me.defaultValue;
+ if (me.convert) {
+ me.calculated = calculated = me.convert.length > 1;
+ me.evil = calculated && !depends;
+ }
+ if (me.persist === null) {
+ me.persist = !calculate;
+ }
+ sortType = me.sortType;
+ if (!me.sortType) {
+ me.sortType = Ext.data.SortTypes.none;
+ } else if (Ext.isString(sortType)) {
+ me.sortType = Ext.data.SortTypes[sortType];
+ }
+ if (depends && typeof depends === 'string') {
+ me.depends = [
+ depends
+ ];
+ }
+ me.cloneDefaultValue = defaultValue !== undefined && (Ext.isDate(defaultValue) || Ext.isArray(defaultValue) || Ext.isObject(defaultValue));
+ },
+ setModelValidators: function(modelValidators) {
+ this._validators = null;
+ this.modelValidators = modelValidators;
+ },
+ constructValidators: function(validators) {
+ var all, length, i, item, validator, presence;
+ if (validators) {
+ if (!(validators instanceof Array)) {
+ validators = [
+ validators
+ ];
+ }
+ all = this._validators;
+ // we are inside getValidators so this is OK
+ for (i = 0 , length = validators.length; i < length; ++i) {
+ item = validators[i];
+ if (item.fn) {
+ item = item.fn;
+ }
+ validator = Ext.Factory.dataValidator(item);
+ if (!validator.isPresence) {
+ all.push(validator);
+ } else {
+ presence = validator;
+ }
+ }
+ if (presence) {
+ this.presence = [
+ presence
+ ];
+ }
+ }
+ },
+ /**
+ * Compares two values to retrieve their relative position in sort order, taking into account
+ * any {@link #sortType}. Also see {@link #compare}.
+ * @param {Object} value1 The first value.
+ * @param {Object} value2 The second value.
+ * @return {Number} `-1` if `value1` is less than `value2`; `1` if `value1` is greater than
+ * `value2`; `0` otherwise.
+ */
+ collate: function(value1, value2) {
+ var me = this,
+ lhs = value1,
+ rhs = value2;
+ if (me.sortType) {
+ lhs = me.sortType(lhs);
+ rhs = me.sortType(rhs);
+ }
+ return (lhs === rhs) ? 0 : ((lhs < rhs) ? -1 : 1);
+ },
+ /**
+ * Compares two values to retrieve their relative position in sort order. Also see
+ * {@link #collate}.
+ * @param {Object} value1 The first value.
+ * @param {Object} value2 The second value.
+ * @return {Number} `-1` if `value1` is less than `value2`; `1` if `value1` is greater than
+ * `value2`; `0` otherwise.
+ */
+ compare: function(value1, value2) {
+ return (value1 === value2) ? 0 : ((value1 < value2) ? -1 : 1);
+ },
+ /**
+ * Tests whether two values are equal based on this field type.
+ * This uses the {@link #compare} method to determine equality, so
+ * this method should generally not be overridden.
+ * @param {Object} value1 The first value.
+ * @param {Object} value2 The second value.
+ * @return {Boolean} `true` if the values are equal.
+ */
+ isEqual: function(value1, value2) {
+ return this.compare(value1, value2) === 0;
+ },
+ /**
+ * A function which converts the value provided by the Reader into the value that will
+ * be stored in the record. This method can be overridden by a derived class or set as
+ * a `{@link #cfg-convert convert}` config.
+ *
+ * If configured as `null`, then no conversion will be applied to the raw data property
+ * when this Field is read. This will increase performance. but you must ensure that
+ * the data is of the correct type and does not *need* converting.
+ *
+ * Example of convert functions:
+ *
+ * function fullName(v, record){
+ * return record.data.last + ', ' + record.data.first;
+ * }
+ *
+ * function location(v, record){
+ * return !record.data.city ? '' : (record.data.city + ', ' + record.data.state);
+ * }
+ *
+ * Ext.define('Dude', {
+ * extend: 'Ext.data.Model',
+ * fields: [
+ * {name: 'fullname', convert: fullName},
+ * {name: 'firstname', mapping: 'name.first'},
+ * {name: 'lastname', mapping: 'name.last'},
+ * {name: 'city', defaultValue: 'unknown'},
+ * 'state',
+ * {name: 'location', convert: location}
+ * ]
+ * });
+ *
+ * // create the data store
+ * var store = Ext.create('Ext.data.Store', {
+ * model: 'Dude',
+ * proxy: {
+ * type: 'memory',
+ * reader: {
+ * type: 'json',
+ * rootProperty: 'daRoot',
+ * totalProperty: 'total'
+ * }
+ * }
+ * });
+ *
+ * var myData = [
+ * { key: 1,
+ * name: { first: 'Fat', last: 'Albert' }
+ * // notice no city, state provided in data object
+ * },
+ * { key: 2,
+ * name: { first: 'Barney', last: 'Rubble' },
+ * city: 'Bedrock', state: 'Stoneridge'
+ * },
+ * { key: 3,
+ * name: { first: 'Cliff', last: 'Claven' },
+ * city: 'Boston', state: 'MA'
+ * }
+ * ];
+ *
+ * @method
+ * @param {Mixed} value The data value as read by the Reader, if undefined will use
+ * the configured `defaultValue`.
+ * @param {Ext.data.Model} record The data object containing the Model as read so far
+ * by the Reader. Note that the Model may not be fully populated at this point as the
+ * fields are read in the order that they are defined.
+ * {@link Ext.data.Model#cfg-fields fields} array.
+ * @return {Mixed} The converted value for storage in the record.
+ */
+ convert: null,
+ /**
+ * A function which converts the Model's value for this Field into a form which can be used
+ * by whatever {@link Ext.data.writer.Writer Writer} is being used to sync data with the server.
+ *
+ * @method
+ * @param {Mixed} value The Field's value - the value to be serialized.
+ * @param {Ext.data.Model} record The record being serialized.
+ * @return {String} The string that represents the Field's value.
+ */
+ serialize: null,
+ /**
+ * Validates the passed value for this field.
+ *
+ * @param {Object} value The value to validate.
+ * @param {String} [separator] This string is passed if the caller wants all validation
+ * messages concatenated with this string between each. This can be handled as a
+ * "falsy" value because concatenating with no separator is seldom desirable.
+ * @param {Ext.data.ErrorCollection/Ext.util.Collection/Array} [errors] This parameter is
+ * passed if the caller wants all validation results individually added to the collection.
+ * @param {Ext.data.Model} record The record being validated
+ * @return {Boolean/String} `true` if the value is valid. A string may be returned if
+ * the value is not valid, to indicate an error message. Any other non `true` value
+ * indicates the value is not valid. This method is not implemented by default,
+ * subclasses may override it to provide an implementation.
+ *
+ * @protected
+ * @template
+ * @since 5.0.0
+ */
+ validate: function(value, separator, errors, record) {
+ var me = this,
+ validators = me.getValidators(),
+ result, presence;
+ presence = this.presence;
+ if (presence && (value == null || value === '')) {
+ result = me.validateGroup(presence, value, separator, errors, record);
+ if (result !== true) {
+ return result;
+ }
+ }
+ return me.validateGroup(validators, value, separator, errors, record);
+ },
+ validateGroup: function(validators, value, separator, errors, record) {
+ var ret = '',
+ validator, length, i, result;
+ for (i = 0 , length = validators.length; i < length; ++i) {
+ validator = validators[i];
+ result = validator.validate(value, record);
+ if (result !== true) {
+ result = result || this.defaultInvalidMessage;
+ if (errors) {
+ if (errors.isMixedCollection) {
+ errors.add(this.name, result);
+ } else if (errors.isCollection) {
+ errors.add(result);
+ } else {
+ errors.push(result);
+ }
+ ret = ret || result;
+ } else if (separator) {
+ if (ret) {
+ ret += separator;
+ }
+ ret += result;
+ } else {
+ ret = result;
+ break;
+ }
+ }
+ }
+ return ret || true;
+ },
+ doCalculate: function(v, rec) {
+ return rec ? this.calculate(rec.data) : v;
+ },
+ /**
+ * Gets the name for this field. See {@link #name}.
+ * @return {String} name
+ */
+ getName: function() {
+ return this.name;
+ },
+ /**
+ * Gets allowBlank for this field. See {@link #allowBlank}.
+ * @return {Boolean} allowBlank
+ */
+ getAllowBlank: function() {
+ return this.allowBlank;
+ },
+ /**
+ * Gets allowNull for this field. See {@link #allowNull}.
+ * @return {Boolean} allowNull
+ */
+ getAllowNull: function() {
+ return this.allowNull;
+ },
+ /**
+ * Gets converter for this field. See {@link #method-convert}.
+ * @return {Function} convert
+ */
+ getConvert: function() {
+ return this.convert;
+ },
+ /**
+ * Gets the defaultValue for this field. See {@link #defaultValue}.
+ * @return {Object} defaultValue
+ */
+ getDefaultValue: function() {
+ return this.defaultValue;
+ },
+ /**
+ * Gets the depends for this field. See {@link #depends}.
+ * @return {String[]} depends
+ */
+ getDepends: function() {
+ return this.depends;
+ },
+ /**
+ * Get the mapping for this field. See {@link #mapping}.
+ * @return {Object} mapping
+ */
+ getMapping: function() {
+ return this.mapping;
+ },
+ /**
+ * Checks if this field has a mapping applied.
+ * @return {Boolean} `true` if this field has a mapping.
+ */
+ hasMapping: function() {
+ var map = this.mapping;
+ return !!(map || map === 0);
+ },
+ /**
+ * Gets the persist for this field. See {@link #persist}.
+ * @return {Boolean} persist
+ */
+ getPersist: function() {
+ return this.persist;
+ },
+ /**
+ * Gets the sortType for this field. See {@link #sortType}.
+ * @return {Function} sortType
+ */
+ getSortType: function() {
+ return this.sortType;
+ },
+ /**
+ * Gets the summary for this field. See {@link #summary}.
+ * @return {Ext.data.summary.Base} The summary.
+ */
+ getSummary: function() {
+ var me = this,
+ doneSummary = me.doneSummary,
+ summary = me.summary;
+ if (!doneSummary) {
+ me.doneSummary = true;
+ if (summary) {
+ me.summary = summary = Ext.Factory.dataSummary(summary);
+ }
+ }
+ return summary || null;
+ },
+ /**
+ * Gets a string representation of the type of this field.
+ * @return {String} type
+ */
+ getType: function() {
+ return 'auto';
+ },
+ privates: {
+ getValidators: function() {
+ var me = this,
+ validators = me._validators;
+ if (!validators) {
+ me._validators = validators = [];
+ me.constructValidators(me.validators);
+ me.constructValidators(me.modelValidators);
+ me.constructValidators(me.instanceValidators);
+ }
+ return validators;
+ }
+ },
+ deprecated: {
+ 5.1: {
+ methods: {
+ /**
+ * @method getSortDir
+ * Gets the sortDir for this field.
+ * @return {String} sortDir
+ * @deprecated 5.1 Setting sortDir and calling getSortDir were never applied by the
+ * the Sorter. This functionality does not natively exist on field instances.
+ */
+ getSortDir: function() {
+ return this.sortDir;
+ }
+ }
+ }
+ }
+});
+
+/**
+ * A data field that automatically {@link #convert converts} its value to a boolean.
+ *
+ * @example
+ * Ext.define('Task', {
+ * extend: 'Ext.data.Model',
+ * fields: [
+ * { name: 'completed', type: 'boolean' }
+ * ]
+ * });
+ *
+ * var record = Ext.create('Task', { completed: true }),
+ * value = record.get('completed');
+ *
+ * Ext.toast("completed is " + value);
+ */
+Ext.define('Ext.data.field.Boolean', {
+ extend: Ext.data.field.Field,
+ alias: [
+ 'data.field.bool',
+ 'data.field.boolean'
+ ],
+ isBooleanField: true,
+ /**
+ * @property [trueRe]
+ * Values matching this regular expression are considered `true`.
+ */
+ trueRe: /^\s*(?:true|yes|on|1)\s*$/i,
+ convert: function(v) {
+ if (typeof v === 'boolean') {
+ return v;
+ }
+ if (this.allowNull && (v === undefined || v === null || v === '')) {
+ return null;
+ }
+ return this.trueRe.test(String(v));
+ },
+ getType: function() {
+ return 'bool';
+ }
+});
+
+/**
+ * A data field that automatically {@link #convert converts} its value to a date.
+ *
+ * @example
+ * Ext.define('User', {
+ * extend: 'Ext.data.Model',
+ * fields: [
+ * { name: 'birthDate', type: 'date' }
+ * ]
+ * });
+ *
+ * var record = Ext.create('User', { birthDate: 'Tuesday, June 27, 2017' }),
+ * value = record.get('birthDate');
+ *
+ * Ext.toast("birthDate is " + value);
+ */
+Ext.define('Ext.data.field.Date', {
+ extend: Ext.data.field.Field,
+ alias: 'data.field.date',
+ sortType: 'asDate',
+ isDateField: true,
+ /**
+ * @cfg {String} dateFormat
+ *
+ * Serves as a default for the {@link #dateReadFormat} and {@link #dateWriteFormat} config
+ * options. This will be used in place of those other configurations if not specified.
+ *
+ * A format string for the {@link Ext.Date#parse Ext.Date.parse} function, or "timestamp"
+ * if the value provided by the Reader is a UNIX timestamp, or "time" if the value provided by
+ * the Reader is a javascript millisecond timestamp. See {@link Ext.Date}.
+ *
+ * It is quite important to note that while this config is optional, it will default to using
+ * the base JavaScript Date object's `parse` function if not specified, rather than
+ * {@link Ext.Date#parse Ext.Date.parse}. This can cause unexpected issues, especially when
+ * converting between timezones, or when converting dates that do not have a timezone specified.
+ * The behavior of the native `Date.parse` is implementation-specific, and depending on the
+ * value of the date string, it might return the UTC date or the local date. __For this reason
+ * it is strongly recommended that you always specify an explicit date format when parsing
+ * dates.__
+ */
+ dateFormat: null,
+ /**
+ * @cfg {String} dateReadFormat
+ * Used when converting received data into a Date when the {@link #type} is specified as
+ * `"date"`. This configuration takes precedence over {@link #dateFormat}.
+ * See {@link #dateFormat} for more information.
+ */
+ dateReadFormat: null,
+ /**
+ * @cfg {String} dateWriteFormat
+ * Provides a custom format when serializing dates with a {@link Ext.data.writer.Writer}.
+ * If this is not specified, the {@link #dateFormat} will be used. If no `dateFormat`
+ * is specified, 'timestamp' format is used.
+ *
+ * See the {@link Ext.data.writer.Writer} docs for more information on writing dates.
+ *
+ * **Note** It is not possible to use the standard date serialization pathway or
+ * {@link Ext#USE_NATIVE_JSON native browser JSON production} to use a
+ * {@link Ext.data.JsonWriter JsonWriter} to send Microsoft formated "JSON" dates.
+ *
+ * To use a {@link Ext.data.JsonWriter JsonWriter} to write dates in a JSON packet in
+ * the form `"\/Date(1357372800000)\/"` configure the field like this:
+ *
+ * {
+ * type: 'date',
+ * dateFormat: 'MS', // To parse incoming dates from server correctly
+ * serialize: null // Avoid formatting or conversion by the Writer
+ * }
+ *
+ * Then override the `Ext.JSON` date serialize function:
+ *
+ * Ext.JSON.encodeDate = function (d) {
+ * return '"' + Ext.Date.format(d, 'MS') + '"';
+ * };
+ */
+ dateWriteFormat: null,
+ /**
+ * @cfg {Boolean} useStrict
+ * @since 6.2.0
+ * Used to manually set strict date parsing on a per-field basis. If no `useStrict`
+ * is specified, will use value of {@link Ext.Date#useStrict} to determine how to
+ * process dates.
+ */
+ compare: function(lhs, rhs) {
+ var lhsIsDate = lhs instanceof Date,
+ rhsIsDate = rhs instanceof Date,
+ result;
+ if (rhsIsDate && lhsIsDate) {
+ result = lhs.getTime() - rhs.getTime();
+ if (result === 0) {
+ result = 0;
+ } else {
+ result = result < 0 ? -1 : 1;
+ }
+ } else if (lhsIsDate === rhsIsDate) {
+ result = 0;
+ } else {
+ result = lhsIsDate ? 1 : -1;
+ }
+ return result;
+ },
+ convert: function(v) {
+ if (!v) {
+ return null;
+ }
+ // instanceof check ~10 times faster than Ext.isDate. Values here will not be
+ // cross-document objects
+ if (v instanceof Date) {
+ return v;
+ }
+ /* eslint-disable-next-line vars-on-top */
+ var dateFormat = this.dateReadFormat || this.dateFormat,
+ parsed;
+ if (dateFormat) {
+ return Ext.Date.parse(v, dateFormat, this.useStrict);
+ }
+ parsed = Date.parse(v);
+ return parsed ? new Date(parsed) : null;
+ },
+ serialize: function(value) {
+ var result = null,
+ format;
+ if (Ext.isDate(value)) {
+ format = this.getDateWriteFormat();
+ result = format ? Ext.Date.format(value, format) : value;
+ }
+ return result;
+ },
+ /**
+ * Gets the dateFormat for this field. See {@link #dateFormat}.
+ * @return {String} dateFormat
+ */
+ getDateFormat: function() {
+ return this.dateFormat;
+ },
+ /**
+ * Gets the dateReadFormat for this field. See {@link #dateReadFormat}.
+ * @return {String} dateReadFormat
+ */
+ getDateReadFormat: function() {
+ return this.dateReadFormat;
+ },
+ /**
+ * Gets the dateWriteFormat for this field. See {@link #dateWriteFormat}.
+ * @return {String} dateWriteFormat
+ */
+ getDateWriteFormat: function() {
+ var me = this;
+ if (me.hasOwnProperty('dateWriteFormat')) {
+ return me.dateWriteFormat;
+ }
+ if (me.hasOwnProperty('dateFormat')) {
+ return me.dateFormat;
+ }
+ return me.dateWriteFormat || me.dateFormat || 'timestamp';
+ },
+ getType: function() {
+ return 'date';
+ }
+});
+
+/**
+ * A data field that automatically {@link #convert converts} its value to an integer.
+ *
+ * **Note:** As you can see in the example below, casting data as an integer
+ * can result in a loss of precision. (5.1 is converted to 5).
+ *
+ * @example
+ * Ext.define('User', {
+ * extend: 'Ext.data.Model',
+ * fields: [
+ * { name: 'age', type: 'integer' }
+ * ]
+ * });
+ *
+ * var record = Ext.create('User', { age: "5.1" }),
+ * value = record.get('age');
+ *
+ * Ext.toast("age is " + value);
+ */
+Ext.define('Ext.data.field.Integer', {
+ extend: Ext.data.field.Field,
+ alias: [
+ 'data.field.int',
+ 'data.field.integer'
+ ],
+ isNumeric: true,
+ isIntegerField: true,
+ numericType: 'int',
+ convert: function(v) {
+ // Handle values which are already numbers.
+ // Value truncation behaviour of parseInt is historic and must be maintained.
+ // parseInt(35.9) and parseInt("35.9") returns 35
+ if (typeof v === 'number') {
+ return this.getNumber(v);
+ }
+ /* eslint-disable-next-line vars-on-top */
+ var empty = v == null || v === '',
+ allowNull = this.allowNull,
+ out;
+ if (empty) {
+ out = allowNull ? null : 0;
+ } else {
+ out = this.parse(v);
+ if (allowNull && isNaN(out)) {
+ out = null;
+ }
+ }
+ return out;
+ },
+ getNumber: function(v) {
+ return parseInt(v, 10);
+ },
+ getType: function() {
+ return this.numericType;
+ },
+ parse: function(v) {
+ return parseInt(String(v).replace(this.stripRe, ''), 10);
+ },
+ sortType: function(s) {
+ // If allowNull, null values needed to be sorted last.
+ if (s == null) {
+ s = Infinity;
+ }
+ return s;
+ }
+});
+
+/**
+ * A data field that automatically {@link #convert converts} its value to a floating-point
+ * number.
+ *
+ * @example
+ * Ext.define('Product', {
+ * extend: 'Ext.data.Model',
+ * fields: [
+ * { name: 'price', type: 'number' }
+ * ]
+ * });
+ *
+ * var record = Ext.create('Product', { price: "5.1" }),
+ * value = record.get('price');
+ *
+ * Ext.toast("price is " + value);
+ */
+Ext.define('Ext.data.field.Number', {
+ extend: Ext.data.field.Integer,
+ alias: [
+ 'data.field.float',
+ 'data.field.number'
+ ],
+ isIntegerField: false,
+ isNumberField: true,
+ numericType: 'float',
+ getNumber: Ext.identityFn,
+ parse: function(v) {
+ return parseFloat(String(v).replace(this.stripRe, ''));
+ }
+});
+
+/**
+ * A data field that automatically {@link #convert converts} its value to a string.
+ *
+ * @example
+ * Ext.define('User', {
+ * extend: 'Ext.data.Model',
+ * fields: [
+ * { name: 'firstName', type: 'string' }
+ * ]
+ * });
+ *
+ * var record = Ext.create('User', { firstName: "Phil" }),
+ * value = record.get('firstName');
+ *
+ * Ext.toast("firstName is " + value);
+ */
+Ext.define('Ext.data.field.String', {
+ extend: Ext.data.field.Field,
+ alias: 'data.field.string',
+ sortType: 'asUCString',
+ isStringField: true,
+ convert: function(v) {
+ var defaultValue = this.allowNull ? null : '';
+ return (v === undefined || v === null) ? defaultValue : String(v);
+ },
+ getType: function() {
+ return 'string';
+ }
+});
+
+/**
+ * This class is a base for all id generators. It also provides lookup of id generators by
+ * their id.
+ *
+ * Generally, id generators are used to generate a primary key for new model instances. There
+ * are different approaches to solving this problem, so this mechanism has both simple use
+ * cases and is open to custom implementations. A {@link Ext.data.Model} requests id generation
+ * using the {@link Ext.data.Model#identifier} property.
+ *
+ * The following types of `identifiers` are provided:
+ *
+ * * `{@link Ext.data.identifier.Sequential sequential}`
+ * * `{@link Ext.data.identifier.Negative negative}`
+ * * `{@link Ext.data.identifier.Uuid uuid}`
+ *
+ * In most cases (other than `uuid`), the server is the only party that can generate
+ * authoritative id values. This means that any id generated by an `identifier` should be
+ * consider "provisional" and must eventually be reconciled with the server. This makes a
+ * `uuid` very attractive as an `identifier` because they are designed to be generated in
+ * a distributed manner and therefore never require reconciliation.
+ *
+ * It is common for id values to be generated as increasing integer values (1, 2, etc.) by
+ * the server when records are inserted. A `{@link Ext.data.identifier.Negative negative}`
+ * `identifier` may be useful as it generates client-side values of -1, -2, etc.. These
+ * values are of the same data type (integer) and so can typically be read by servers
+ * using typed languages (such as Java or C#) and easily recognized as provisional.
+ *
+ * In the end, the choice of `identifier` strategy requires agreement between client and
+ * server.
+ *
+ * # Identity, Type and Shared Generators
+ *
+ * It is often desirable to share Generators to ensure uniqueness or common configuration.
+ * This is done by giving Generator instances an id property by which they can be looked
+ * up using the {@link Ext.Factory#dataIdentifier dataIdentifier} method. To configure two
+ * {@link Ext.data.Model Model} classes to share one
+ * {@link Ext.data.identifier.Sequential sequential} id generator, you simply assign them
+ * the same id:
+ *
+ * Ext.define('MyApp.data.MyModelA', {
+ * extend: 'Ext.data.Model',
+ * identifier: {
+ * type: 'sequential',
+ * id: 'foo'
+ * }
+ * });
+ *
+ * Ext.define('MyApp.data.MyModelB', {
+ * extend: 'Ext.data.Model',
+ * identifier: {
+ * type: 'sequential',
+ * id: 'foo'
+ * }
+ * });
+ *
+ * To make this as simple as possible for generator types that are shared by many (or all)
+ * Models, the Generator types (such as 'sequential' or 'uuid') are also reserved as
+ * generator ids. This is used by the {@link Ext.data.identifier.Uuid} which has an id equal
+ * to its type ('uuid'). In other words, the following Models share the same generator:
+ *
+ * Ext.define('MyApp.data.MyModelX', {
+ * extend: 'Ext.data.Model',
+ * identifier: 'uuid'
+ * });
+ *
+ * Ext.define('MyApp.data.MyModelY', {
+ * extend: 'Ext.data.Model',
+ * identifier: 'uuid'
+ * });
+ *
+ * This can be overridden (by specifying the id explicitly), but there is no particularly
+ * good reason to do so for this generator type.
+ *
+ * # Creating Custom Generators
+ *
+ * An id generator should derive from this class and implement the {@link #generate} method.
+ *
+ * To register an id generator type, a derived class should provide an `alias` like so:
+ *
+ * Ext.define('MyApp.data.identifier.Custom', {
+ * extend: 'Ext.data.identifier.Generator',
+ * alias: 'data.identifier.custom',
+ * config: {
+ * configProp: 42 // some config property w/default value
+ * }
+ *
+ * generate: function () {
+ * return ... // a new id
+ * }
+ * });
+ *
+ * Using the custom id generator is then straightforward:
+ *
+ * Ext.define('MyApp.data.MyModel', {
+ * extend: 'Ext.data.Model',
+ * identifier: 'custom'
+ * });
+ * // or...
+ *
+ * Ext.define('MyApp.data.MyModel', {
+ * extend: 'Ext.data.Model',
+ * identifier: {
+ * type: 'custom',
+ * configProp: value
+ * }
+ * });
+ *
+ * It is not recommended to mix shared generators with generator configuration. This leads
+ * to unpredictable results unless all configurations match (which is also redundant). In
+ * such cases, a custom generator with a default id is the best approach.
+ *
+ * Ext.define('MyApp.data.identifier.Custom', {
+ * extend: 'Ext.data.identifier.Sequential',
+ * alias: 'data.identifier.custom',
+ *
+ * config: {
+ * id: 'custom',
+ * prefix: 'ID_',
+ * seed: 1000
+ * }
+ * });
+ *
+ * Ext.define('MyApp.data.MyModelX', {
+ * extend: 'Ext.data.Model',
+ * identifier: 'custom'
+ * });
+ *
+ * Ext.define('MyApp.data.MyModelY', {
+ * extend: 'Ext.data.Model',
+ * identifier: 'custom'
+ * });
+ *
+ * // the above models share a generator that produces ID_1000, ID_1001, etc..
+ *
+ */
+Ext.define('Ext.data.identifier.Generator', {
+ 'abstract': true,
+ mixins: [
+ Ext.mixin.Factoryable
+ ],
+ alias: 'data.identifier.default',
+ // this is used by Factoryable
+ factoryConfig: {
+ defaultType: 'sequential'
+ },
+ // this is not a suitable type to create
+ /**
+ * @property {Boolean} isGenerator
+ * `true` in this class to identify an object as an instantiated IdGenerator, or subclass
+ * thereof.
+ */
+ isGenerator: true,
+ config: {
+ /**
+ * @cfg {String} id
+ * The id for this generator.
+ */
+ id: null
+ },
+ /**
+ * Initializes a new instance.
+ * @param {Object} config (optional) Configuration object to be applied to the new instance.
+ */
+ constructor: function(config) {
+ var me = this,
+ id;
+ me.initConfig(config);
+ id = me.getId();
+ if (id) {
+ Ext.data.identifier.Generator.all[id] = me;
+ }
+ },
+ /**
+ * Generates and returns the next id. This method must be implemented by the derived
+ * class.
+ *
+ * @return {Number/String} The next id.
+ * @method generate
+ * @abstract
+ */
+ privates: {
+ /**
+ * Create a copy of this identifier.
+ * @private
+ * @return {Ext.data.identifier.Generator} The clone
+ */
+ clone: function(config) {
+ var cfg = this.getInitialConfig();
+ cfg = config ? Ext.apply({}, config, cfg) : cfg;
+ return new this.self(cfg);
+ },
+ statics: {
+ /**
+ * @property {Object} all
+ * This object is keyed by id to lookup instances.
+ * @private
+ * @static
+ */
+ all: {}
+ }
+ }
+}, function() {
+ var Generator = this,
+ Factory = Ext.Factory,
+ factory = Factory.dataIdentifier;
+ // If there is an id property passed we need to lookup that id in the cache. If that
+ // produces a cache miss, call the normal factory.
+ /**
+ * @member Ext.Factory
+ * @method dataIdentifier
+ * Returns an instance of an ID generator based on the ID you pass in.
+ * @param {String/Object} config The config object or `id` to lookup.
+ * @return {Object} Ext.data.identifier.* The data identifier
+ */
+ Factory.dataIdentifier = function(config) {
+ var id = Ext.isString(config) ? config : (config && config.id),
+ existing = id && Generator.all[id];
+ return existing || factory(config);
+ };
+});
+
+/**
+ * This class is a sequential id generator. A simple use of this class would be like so:
+ *
+ * Ext.define('MyApp.data.MyModel', {
+ * extend: 'Ext.data.Model',
+ * identifier: 'sequential'
+ * });
+ * // assign id's of 1, 2, 3, etc.
+ *
+ * An example of a configured generator would be:
+ *
+ * Ext.define('MyApp.data.MyModel', {
+ * extend: 'Ext.data.Model',
+ * identifier: {
+ * type: 'sequential',
+ * prefix: 'ID_',
+ * seed: 1000,
+ * increment: 10
+ * }
+ * });
+ * // assign id's of ID_1000, ID_1010, ID_1020, etc.
+ *
+ */
+Ext.define('Ext.data.identifier.Sequential', {
+ extend: Ext.data.identifier.Generator,
+ alias: 'data.identifier.sequential',
+ config: {
+ /**
+ * @cfg {Number} increment
+ * The number by which to adjust the `seed` after for the next sequential id.
+ */
+ increment: 1,
+ /**
+ * @cfg {String} prefix
+ * The string to place in front of the sequential number for each generated id.
+ */
+ prefix: null,
+ /**
+ * @cfg {Number} seed
+ * The number at which to start generating sequential id's.
+ */
+ seed: 1
+ },
+ /**
+ * Generates and returns the next id.
+ * @return {String/Number} The next id. If a {@link #prefix} was specified, returns
+ * a String, otherwise returns a Number.
+ */
+ generate: function() {
+ var me = this,
+ seed = me._seed,
+ prefix = me._prefix;
+ me._seed += me._increment;
+ return (prefix !== null) ? prefix + seed : seed;
+ }
+});
+
+/**
+ * A Model or Entity represents some object that your application manages. For example, one
+ * might define a Model for Users, Products, Cars, or other real-world object that we want
+ * to model in the system. Models are used by {@link Ext.data.Store stores}, which are in
+ * turn used by many of the data-bound components in Ext.
+ *
+ * # Fields
+ *
+ * Models are defined as a set of fields and any arbitrary methods and properties relevant
+ * to the model. For example:
+ *
+ * Ext.define('User', {
+ * extend: 'Ext.data.Model',
+ * fields: [
+ * {name: 'name', type: 'string'},
+ * {name: 'age', type: 'int', convert: null},
+ * {name: 'phone', type: 'string'},
+ * {name: 'alive', type: 'boolean', defaultValue: true, convert: null}
+ * ],
+ *
+ * changeName: function() {
+ * var oldName = this.get('name'),
+ * newName = oldName + " The Barbarian";
+ *
+ * this.set('name', newName);
+ * }
+ * });
+ *
+ * Now we can create instances of our User model and call any model logic we defined:
+ *
+ * var user = Ext.create('User', {
+ * id : 'ABCD12345',
+ * name : 'Conan',
+ * age : 24,
+ * phone: '555-555-5555'
+ * });
+ *
+ * user.changeName();
+ * user.get('name'); //returns "Conan The Barbarian"
+ *
+ * By default, the built in field types such as number and boolean coerce string values
+ * in the raw data by virtue of their {@link Ext.data.field.Field#method-convert} method.
+ * When the server can be relied upon to send data in a format that does not need to be
+ * converted, disabling this can improve performance. The {@link Ext.data.reader.Json Json}
+ * and {@link Ext.data.reader.Array Array} readers are likely candidates for this
+ * optimization. To disable field conversions you simply specify `null` for the field's
+ * {@link Ext.data.field.Field#cfg-convert convert config}.
+ *
+ * ## The "id" Field and `idProperty`
+ *
+ * A Model definition always has an *identifying field* which should yield a unique key
+ * for each instance. By default, a field named "id" will be created with a
+ * {@link Ext.data.Field#mapping mapping} of "id". This happens because of the default
+ * {@link #idProperty} provided in Model definitions.
+ *
+ * To alter which field is the identifying field, use the {@link #idProperty} config.
+ *
+ * # Validators
+ *
+ * Models have built-in support for field validators. Validators are added to models as in
+ * the follow example:
+ *
+ * Ext.define('User', {
+ * extend: 'Ext.data.Model',
+ * fields: [
+ * { name: 'name', type: 'string' },
+ * { name: 'age', type: 'int' },
+ * { name: 'phone', type: 'string' },
+ * { name: 'gender', type: 'string' },
+ * { name: 'username', type: 'string' },
+ * { name: 'alive', type: 'boolean', defaultValue: true }
+ * ],
+ *
+ * validators: {
+ * age: 'presence',
+ * name: { type: 'length', min: 2 },
+ * gender: { type: 'inclusion', list: ['Male', 'Female'] },
+ * username: [
+ * { type: 'exclusion', list: ['Admin', 'Operator'] },
+ * { type: 'format', matcher: /([a-z]+)[0-9]{2,3}/i }
+ * ]
+ * }
+ * });
+ *
+ * The derived type of `Ext.data.field.Field` can also provide validation. If `validators`
+ * need to be duplicated on multiple fields, instead consider creating a custom field type.
+ *
+ * ## Validation
+ *
+ * The results of the validators can be retrieved via the "associated" validation record:
+ *
+ * var instance = Ext.create('User', {
+ * name: 'Ed',
+ * gender: 'Male',
+ * username: 'edspencer'
+ * });
+ *
+ * var validation = instance.getValidation();
+ *
+ * The returned object is an instance of `Ext.data.Validation` and has as its fields the
+ * result of the field `validators`. The validation object is "dirty" if there are one or
+ * more validation errors present.
+ *
+ * This record is also available when using data binding as a "pseudo-association" called
+ * "validation". This pseudo-association can be hidden by an explicitly declared
+ * association by the same name (for compatibility reasons), but doing so is not
+ * recommended.
+ *
+ * The `{@link Ext.Component#modelValidation}` config can be used to enable automatic
+ * binding from the "validation" of a record to the form fields that may be bound to its
+ * values.
+ *
+ * # Associations
+ *
+ * Models often have associations with other Models. These associations can be defined by
+ * fields (often called "foreign keys") or by other data such as a many-to-many (or "matrix").
+ * See {@link Ext.data.schema.Association} for information about configuring and using associations.
+ *
+ * # Using a Proxy
+ *
+ * Models are great for representing types of data and relationships, but sooner or later we're
+ * going to want to load or save that data somewhere. All loading and saving of data is handled
+ * via a {@link Ext.data.proxy.Proxy Proxy}, which can be set directly on the Model:
+ *
+ * Ext.define('User', {
+ * extend: 'Ext.data.Model',
+ * fields: ['id', 'name', 'email'],
+ *
+ * proxy: {
+ * type: 'rest',
+ * url : '/users'
+ * }
+ * });
+ *
+ * Here we've set up a {@link Ext.data.proxy.Rest Rest Proxy}, which knows how to load and save
+ * data to and from a RESTful backend. Let's see how this works:
+ *
+ * var user = Ext.create('User', {name: 'Ed Spencer', email: 'ed@sencha.com'});
+ *
+ * user.save(); //POST /users
+ *
+ * Calling {@link #save} on the new Model instance tells the configured RestProxy that we wish to
+ * persist this Model's data onto our server. RestProxy figures out that this Model hasn't been
+ * saved before because it doesn't have an id, and performs the appropriate action - in this case
+ * issuing a POST request to the url we configured (/users). We configure any Proxy on any Model
+ * and always follow this API - see {@link Ext.data.proxy.Proxy} for a full list.
+ *
+ * Loading data via the Proxy is accomplished with the static `load` method:
+ *
+ * // Uses the configured RestProxy to make a GET request to /users/123
+ * User.load(123, {
+ * success: function(user) {
+ * console.log(user.getId()); //logs 123
+ * }
+ * });
+ *
+ * Models can also be updated and destroyed easily:
+ *
+ * // the user Model we loaded in the last snippet:
+ * user.set('name', 'Edward Spencer');
+ *
+ * // tells the Proxy to save the Model. In this case it will perform a PUT request
+ * // to /users/123 as this Model already has an id
+ * user.save({
+ * success: function() {
+ * console.log('The User was updated');
+ * }
+ * });
+ *
+ * // tells the Proxy to destroy the Model. Performs a DELETE request to /users/123
+ * user.erase({
+ * success: function() {
+ * console.log('The User was destroyed!');
+ * }
+ * });
+ *
+ * # HTTP Parameter names when using a {@link Ext.data.proxy.Ajax Ajax proxy}
+ *
+ * By default, the model ID is specified in an HTTP parameter named `id`. To change the
+ * name of this parameter use the Proxy's {@link Ext.data.proxy.Ajax#idParam idParam}
+ * configuration.
+ *
+ * Parameters for other commonly passed values such as
+ * {@link Ext.data.proxy.Ajax#pageParam page number} or
+ * {@link Ext.data.proxy.Ajax#startParam start row} may also be configured.
+ *
+ * # Usage in Stores
+ *
+ * It is very common to want to load a set of Model instances to be displayed and manipulated
+ * in the UI. We do this by creating a {@link Ext.data.Store Store}:
+ *
+ * var store = Ext.create('Ext.data.Store', {
+ * model: 'User'
+ * });
+ *
+ * //uses the Proxy we set up on Model to load the Store data
+ * store.load();
+ *
+ * A Store is just a collection of Model instances - usually loaded from a server somewhere. Store
+ * can also maintain a set of added, updated and removed Model instances to be synchronized with
+ * the server via the Proxy. See the {@link Ext.data.Store Store docs} for more information
+ * on Stores.
+ */
+Ext.define('Ext.data.Model', {
+ alternateClassName: 'Ext.data.Record',
+ /**
+ * @property {Boolean} isEntity
+ * The value `true` to identify this class and its subclasses.
+ * @readonly
+ */
+ isEntity: true,
+ /**
+ * @property {Boolean} isModel
+ * The value `true` to identify this class and its subclasses.
+ * @readonly
+ */
+ isModel: true,
+ // Record ids are more flexible.
+ validIdRe: null,
+ erasing: false,
+ loadOperation: null,
+ loadCount: 0,
+ observableType: 'record',
+ /**
+ * @property {"C"/"R"/"U"/"D"} crudState
+ * This value is initially "R" or "C" indicating the initial CRUD state. As the
+ * record changes and the various joined parties (stores, sessions, etc.) are notified
+ * this value is updated prior to these calls. In other words, the joined parties
+ * are notified after the `crudState` is updated. This means that the `crudState`
+ * property may be briefly out of sync with underlying changes if this state is used
+ * outside of these notifications.
+ *
+ * The possible states have these meanings:
+ *
+ * * "R" - The record is in a cleanly retrieved (unmodified) state.
+ * * "C" - The record is in a newly created (`phantom`) state.
+ * * "U" - The record is in an updated, `modified` (`dirty`) state.
+ * * "D" - The record is in a `dropped` state.
+ *
+ * @readonly
+ * @protected
+ * @since 6.2.0
+ */
+ crudState: 'R',
+ /**
+ * @property {"C"/"R"/"U"/"D"} crudStateWas
+ * This value is initially `null` indicating there is no previous CRUD state. As the
+ * record changes and the various joined parties (stores, sessions, etc.) are notified
+ * this value is updated for the *subsequent* calls. In other words, the joined parties
+ * are notified and then `crudStateWas` is modified for the next update.
+ *
+ * The value of this property has the same meaning as `crudState`.
+ *
+ * @readonly
+ * @protected
+ * @since 6.2.0
+ */
+ crudStateWas: null,
+ constructor: function(data, session, skipStoreAddition) {
+ var me = this,
+ cls = me.self,
+ identifier = cls.identifier,
+ Model = Ext.data.Model,
+ modelIdentifier = Model.identifier,
+ idProperty = me.idField.name,
+ array, id, initializeFn, internalId, len, i, fields;
+ // Yes, this is here on purpose. See EXTJS-16494. The second
+ // assignment seems to work around a strange JIT issue that prevents
+ // this.data being assigned in random scenarios, even though the data
+ // is passed into the constructor. The issue occurs on 4th gen iPads and
+ // lower, possibly other older iOS devices.
+ // A similar issue can occur with the hasListeners property of Observable
+ // (see the constructor of Ext.mixin.Observable)
+ me.data = me.data = data || (data = {});
+ me.internalId = internalId = modelIdentifier.generate();
+ var dataId = data[idProperty];
+ // eslint-disable-line vars-on-top, one-var
+ if (session && !session.isSession) {
+ Ext.raise('Bad Model constructor argument 2 - "session" is not a Session');
+ }
+ if ((array = data) instanceof Array) {
+ me.data = data = {};
+ fields = me.getFields();
+ len = Math.min(fields.length, array.length);
+ for (i = 0; i < len; ++i) {
+ data[fields[i].name] = array[i];
+ }
+ }
+ if (!(initializeFn = cls.initializeFn)) {
+ cls.initializeFn = initializeFn = Model.makeInitializeFn(cls);
+ }
+ if (!initializeFn.$nullFn) {
+ cls.initializeFn(me);
+ }
+ // Must do this after running the initializeFn due to converters on idField
+ if (!me.isSummaryModel) {
+ if (!(me.id = id = data[idProperty]) && id !== 0) {
+ if (dataId) {
+ Ext.raise('The model ID configured in data ("' + dataId + '") has been rejected by the ' + me.fieldsMap[idProperty].type + ' field converter for the ' + idProperty + ' field');
+ }
+ if (session) {
+ identifier = session.getIdentifier(cls);
+ id = identifier.generate();
+ } else if (modelIdentifier === identifier) {
+ id = internalId;
+ } else {
+ id = identifier.generate();
+ }
+ data[idProperty] = me.id = id;
+ me.phantom = true;
+ me.crudState = 'C';
+ }
+ if (session && !skipStoreAddition) {
+ session.add(me);
+ }
+ // Needs to be set after the add to the session
+ if (me.phantom) {
+ me.crudStateWas = 'C';
+ }
+ }
+ if (me.init && Ext.isFunction(me.init)) {
+ me.init();
+ }
+ },
+ /**
+ * @property {String} entityName
+ * The short name of this entity class. This name is derived from the `namespace` of
+ * the associated `schema` and this class name. By default, a class is not given a
+ * shortened name.
+ *
+ * All entities in a given `schema` must have a unique `entityName`.
+ *
+ * For more details see "Relative Naming" in {@link Ext.data.schema.Schema}.
+ */
+ /**
+ * @property {Boolean} editing
+ * Internal flag used to track whether or not the model instance is currently being edited.
+ * @readonly
+ */
+ editing: false,
+ /**
+ * @property {Boolean} dirty
+ * True if this record has been modified.
+ * @readonly
+ */
+ dirty: false,
+ /**
+ * @property {Ext.data.Session} session
+ * The {@link Ext.data.Session} for this record.
+ * @readonly
+ */
+ session: null,
+ /**
+ * @property {Boolean} dropped
+ * True if this record is pending delete on the server. This is set by the `drop`
+ * method and transmitted to the server by the `save` method.
+ * @readonly
+ */
+ dropped: false,
+ /**
+ * @property {Boolean} erased
+ * True if this record has been erased on the server. This flag is set of the `erase`
+ * method.
+ * @readonly
+ */
+ erased: false,
+ /**
+ * @cfg {String} clientIdProperty
+ * The name of the property a server will use to send back a client-generated id in a
+ * `create` or `update` `{@link Ext.data.operation.Operation operation}`.
+ *
+ * If specified, this property cannot have the same name as any other field.
+ *
+ * For example:
+ *
+ * Ext.define('Person', {
+ * idProperty: 'id', // this is the default value (for clarity)
+ *
+ * clientIdProperty: 'clientId',
+ *
+ * identifier: 'negative', // to generate -1, -2 etc on the client
+ *
+ * fields: [ 'name' ]
+ * });
+ *
+ * var person = new Person({
+ * // no id provided, so -1 is generated
+ * name: 'Clark Kent'
+ * });
+ *
+ * The server is given this data during the `create`:
+ *
+ * {
+ * id: -1,
+ * name: 'Clark Kent'
+ * }
+ *
+ * The server allocates a real id and responds like so:
+ *
+ * {
+ * id: 427,
+ * clientId: -1
+ * }
+ *
+ * This property is most useful when creating multiple entities in a single call to
+ * the server in a `{@link Ext.data.operation.Create create operation}`. Alternatively,
+ * the server could respond with records that correspond one-to-one to those sent in
+ * the `operation`.
+ *
+ * For example the client could send a `create` with this data:
+ *
+ * [ { id: -1, name: 'Clark Kent' },
+ * { id: -2, name: 'Peter Parker' },
+ * { id: -3, name: 'Bruce Banner' } ]
+ *
+ * And the server could respond in the same order:
+ *
+ * [ { id: 427 }, // updates id = -1
+ * { id: 428 }, // updates id = -2
+ * { id: 429 } ] // updates id = -3
+ *
+ * Or using `clientIdProperty` the server could respond in arbitrary order:
+ *
+ * [ { id: 427, clientId: -3 },
+ * { id: 428, clientId: -1 },
+ * { id: 429, clientId: -2 } ]
+ *
+ * **IMPORTANT:** When upgrading from previous versions be aware that this property
+ * used to perform the role of `{@link Ext.data.writer.Writer#clientIdProperty}` as
+ * well as that described above. To continue send a client-generated id as other than
+ * the `idProperty`, set `clientIdProperty` on the `writer`. A better solution, however,
+ * is most likely a properly configured `identifier` as that would work better with
+ * associations.
+ */
+ clientIdProperty: null,
+ evented: false,
+ /**
+ * @property {Boolean} phantom
+ * True when the record does not yet exist in a server-side database. Any record which
+ * has a real database identity set as its `idProperty` is NOT a phantom -- it's real.
+ */
+ phantom: false,
+ /**
+ * @cfg {String} idProperty
+ * The name of the field treated as this Model's unique id.
+ *
+ * If changing the idProperty in a subclass, the generated id field will replace the
+ * one generated by the superclass, for example;
+ *
+ * Ext.define('Super', {
+ * extend: 'Ext.data.Model',
+ * fields: ['name']
+ * });
+ *
+ * Ext.define('Sub', {
+ * extend: 'Super',
+ * idProperty: 'customId'
+ * });
+ *
+ * var fields = Super.getFields();
+ * // Has 2 fields, "name" & "id"
+ * console.log(fields[0].name, fields[1].name, fields.length);
+ *
+ * fields = Sub.getFields();
+ * // Has 2 fields, "name" & "customId", "id" is replaced
+ * console.log(fields[0].name, fields[1].name, fields.length);
+ *
+ * The data values for this field must be unique or there will be id value collisions
+ * in the {@link Ext.data.Store Store}.
+ */
+ idProperty: 'id',
+ /**
+ * @cfg {Object} manyToMany
+ * A config object for a {@link Ext.data.schema.ManyToMany ManyToMany} association.
+ * See the class description for {@link Ext.data.schema.ManyToMany ManyToMany} for
+ * configuration examples.
+ */
+ manyToMany: null,
+ /**
+ * @cfg {String/Object} identifier
+ * The id generator to use for this model. The `identifier` generates values for the
+ * {@link #idProperty} when no value is given. Records with client-side generated
+ * values for {@link #idProperty} are called {@link #phantom} records since they are
+ * not yet known to the server.
+ *
+ * This can be overridden at the model level to provide a custom generator for a
+ * model. The simplest form of this would be:
+ *
+ * Ext.define('MyApp.data.MyModel', {
+ * extend: 'Ext.data.Model',
+ * requires: ['Ext.data.identifier.Sequential'],
+ * identifier: 'sequential',
+ * ...
+ * });
+ *
+ * The above would generate {@link Ext.data.identifier.Sequential sequential} id's
+ * such as 1, 2, 3 etc..
+ *
+ * Another useful id generator is {@link Ext.data.identifier.Uuid}:
+ *
+ * Ext.define('MyApp.data.MyModel', {
+ * extend: 'Ext.data.Model',
+ * requires: ['Ext.data.identifier.Uuid'],
+ * identifier: 'uuid',
+ * ...
+ * });
+ *
+ * An id generator can also be further configured:
+ *
+ * Ext.define('MyApp.data.MyModel', {
+ * extend: 'Ext.data.Model',
+ * identifier: {
+ * type: 'sequential',
+ * seed: 1000,
+ * prefix: 'ID_'
+ * }
+ * });
+ *
+ * The above would generate id's such as ID_1000, ID_1001, ID_1002 etc..
+ *
+ * If multiple models share an id space, a single generator can be shared:
+ *
+ * Ext.define('MyApp.data.MyModelX', {
+ * extend: 'Ext.data.Model',
+ * identifier: {
+ * type: 'sequential',
+ * id: 'xy'
+ * }
+ * });
+ *
+ * Ext.define('MyApp.data.MyModelY', {
+ * extend: 'Ext.data.Model',
+ * identifier: {
+ * type: 'sequential',
+ * id: 'xy'
+ * }
+ * });
+ *
+ * For more complex, shared id generators, a custom generator is the best approach.
+ * See {@link Ext.data.identifier.Generator} for details on creating custom id
+ * generators.
+ */
+ identifier: null,
+ // Fields config and property
+ // @cmd-auto-dependency {aliasPrefix: "data.field."}
+ /**
+ * @cfg {Object[]/String[]} fields
+ * An Array of `Ext.data.field.Field` config objects, simply the field
+ * {@link Ext.data.field.Field#name name}, or a mix of config objects and strings.
+ * If just a name is given, the field type defaults to `auto`.
+ *
+ * In a {@link Ext.data.field.Field Field} config object you may pass the alias of
+ * the `Ext.data.field.*` type using the `type` config option.
+ *
+ * // two fields are set:
+ * // - an 'auto' field with a name of 'firstName'
+ * // - and an Ext.data.field.Integer field with a name of 'age'
+ * fields: ['firstName', {
+ * type: 'int',
+ * name: 'age'
+ * }]
+ *
+ * Fields will automatically be created at read time for any for any keys in the
+ * data passed to the Model's {@link #proxy proxy's}
+ * {@link Ext.data.reader.Reader reader} whose name is not explicitly configured in
+ * the `fields` config.
+ *
+ * Extending a Model class will inherit all the `fields` from the superclass /
+ * ancestor classes.
+ */
+ /**
+ * @property {Ext.data.field.Field[]} fields
+ * An array fields defined for this Model (including fields defined in superclasses)
+ * in ordinal order; that is in declaration order.
+ * @private
+ * @readonly
+ */
+ /**
+ * @property {Object} fieldOrdinals
+ * This property is indexed by field name and contains the ordinal of that field. The
+ * ordinal often has meaning to servers and is derived based on the position in the
+ * `fields` array.
+ *
+ * This can be used like so:
+ *
+ * Ext.define('MyApp.models.User', {
+ * extend: 'Ext.data.Model',
+ *
+ * fields: [
+ * { name: 'name' }
+ * ]
+ * });
+ *
+ * var nameOrdinal = MyApp.models.User.fieldOrdinals.name;
+ *
+ * // or, if you have an instance:
+ *
+ * var user = new MyApp.models.User();
+ * var nameOrdinal = user.fieldOrdinals.name;
+ *
+ * @private
+ * @readonly
+ */
+ /**
+ * @property {Object} modified
+ * A hash of field values which holds the initial values of fields before a set of
+ * edits are {@link #commit committed}.
+ */
+ /**
+ * @property {Object} previousValues
+ * This object is similar to the `modified` object except it holds the data values as
+ * they were prior to the most recent change.
+ * @readonly
+ * @private
+ */
+ previousValues: undefined,
+ // Not "null" so getPrevious returns undefined first time
+ // @cmd-auto-dependency { aliasPrefix : "proxy.", defaultPropertyName : "defaultProxyType"}
+ /**
+ * @cfg {String/Object/Ext.data.proxy.Proxy} proxy
+ * The {@link Ext.data.proxy.Proxy proxy} to use for this class.
+ *
+ * By default, the proxy is configured from the {@link Ext.data.schema.Schema schema}.
+ * You can ignore the schema defaults by setting `schema: false` on the `proxy` config.
+ *
+ * Ext.define('MyApp.data.CustomProxy', {
+ * extend: 'Ext.data.proxy.Ajax',
+ * alias: 'proxy.customproxy',
+ *
+ * url: 'users.json'
+ * });
+ *
+ * Ext.define('MyApp.models.CustomModel', {
+ * extend: 'Ext.data.Model',
+ *
+ * fields: ['name'],
+ * proxy: {
+ * type: 'customproxy,
+ * schema: false
+ * }
+ * });
+ *
+ * With `schema: false`, the `url` of the proxy will be used instead of what has been defined
+ * on the schema.
+ */
+ proxy: undefined,
+ /**
+ * @cfg {String/Object} [schema='default']
+ * The name of the {@link Ext.data.schema.Schema schema} to which this entity and its
+ * associations belong. For details on custom schemas see `Ext.data.schema.Schema`.
+ */
+ /**
+ * @property {Ext.data.schema.Schema} schema
+ * The `Ext.data.schema.Schema` to which this entity and its associations belong.
+ * @readonly
+ */
+ schema: 'default',
+ /**
+ * @cfg {Object} summary
+ * Summary fields are a special kind of field that is used to assist in creating an
+ * aggregation for this model. A new model type that extends this model will be
+ * created, accessible via {@link #method-getSummaryModel}. This summary model will
+ * have these virtual aggregate fields in the fields collection like a normal model.
+ * Each key in the object is the field name. The value for each field should mirror
+ * the {@link #cfg-fields}, excluding the `name` option. The summary model generated
+ * will have 2 fields, 'rate', which will aggregate using an average and maxRate,
+ * which will aggregate using the maximum value.
+ *
+ * See {@link Ext.data.summary.Base} for more information.
+ *
+ * Ext.define('User', {
+ * extend: 'Ext.data.Model',
+ * fields: [{
+ * name: 'rate',
+ * summary: 'avg'
+ * }],
+ *
+ * summary: {
+ * maxRate: {
+ * field: 'rate', // calculated from rate field
+ * summary: 'max'
+ * }
+ * }
+ * });
+ *
+ * @since 6.5.0
+ */
+ summary: null,
+ /**
+ * @cfg {String} versionProperty
+ * If specified, this is the name of the property that contains the entity "version".
+ * The version property is used to manage a long-running transaction and allows the
+ * detection of simultaneous modification.
+ *
+ * The way a version property is used is that the client receives the version as it
+ * would any other entity property. When saving an entity, this property is always
+ * included in the request and the server uses the value in a "conditional update".
+ * If the current version of the entity on the server matches the version property
+ * sent by the client, the update is allowed. Otherwise, the update fails.
+ *
+ * On successful update, both the client and server increment the version. This is
+ * done on the server in the conditional update and on the client when it receives a
+ * success on its update request.
+ */
+ versionProperty: null,
+ /**
+ * @property {Number} generation
+ * This property is incremented on each modification of a record.
+ * @readonly
+ * @since 5.0.0
+ */
+ generation: 1,
+ /**
+ * @cfg {Object[]} validators
+ * An array of {@link Ext.data.validator.Validator validators} for this model.
+ */
+ /**
+ * @cfg {String} validationSeparator
+ * If specified this property is used to concatenate multiple errors for each field
+ * as reported by the `validators`.
+ */
+ validationSeparator: null,
+ /**
+ * @cfg {Boolean} convertOnSet
+ * Set to `false` to prevent any converters from being called on fields specified in
+ * a {@link Ext.data.Model#set set} operation.
+ *
+ * **Note:** Setting the config to `false` will only prevent the convert / calculate
+ * call when the set `fieldName` param matches the field's `{@link #name}`. In the
+ * following example the calls to set `salary` will not execute the convert method
+ * on `set` while the calls to set `vested` will execute the convert method on the
+ * initial read as well as on `set`.
+ *
+ * Example model definition:
+ *
+ * Ext.define('MyApp.model.Employee', {
+ * extend: 'Ext.data.Model',
+ * fields: ['yearsOfService', {
+ * name: 'salary',
+ * convert: function (val) {
+ * var startingBonus = val * .1;
+ * return val + startingBonus;
+ * }
+ * }, {
+ * name: 'vested',
+ * convert: function (val, record) {
+ * return record.get('yearsOfService') >= 4;
+ * },
+ * depends: 'yearsOfService'
+ * }],
+ * convertOnSet: false
+ * });
+ *
+ * var tina = Ext.create('MyApp.model.Employee', {
+ * salary: 50000,
+ * yearsOfService: 3
+ * });
+ *
+ * console.log(tina.get('salary')); // logs 55000
+ * console.log(tina.get('vested')); // logs false
+ *
+ * tina.set({
+ * salary: 60000,
+ * yearsOfService: 4
+ * });
+ * console.log(tina.get('salary')); // logs 60000
+ * console.log(tina.get('vested')); // logs true
+ */
+ convertOnSet: true,
+ // Associations configs and properties
+ /**
+ * @cfg {Object[]} associations
+ * An array of {@link Ext.data.schema.Association associations} for this model.
+ *
+ * For further documentation, see {@link Ext.data.schema.Association}.
+ *
+ * @deprecated 6.2.0 Use `hasMany/hasOne/belongsTo`.
+ */
+ /**
+ * @cfg {String/Object/String[]/Object[]} hasMany
+ * One or more `Ext.data.schema.HasMany` associations for this model.
+ */
+ /**
+ * @cfg {String/Object/String[]/Object[]} hasOne
+ * One or more `Ext.data.schema.HasOne` associations for this model.
+ */
+ /**
+ * @cfg {String/Object/String[]/Object[]} belongsTo
+ * One or more `Ext.data.schema.BelongsTo` associations for this model.
+ */
+ /**
+ * Begins an edit. While in edit mode, no events (e.g.. the `update` event) are
+ * relayed to the containing store. When an edit has begun, it must be followed by
+ * either `endEdit` or `cancelEdit`.
+ */
+ beginEdit: function() {
+ var me = this,
+ modified = me.modified,
+ previousValues = me.previousValues;
+ if (!me.editing) {
+ me.editing = true;
+ me.editMemento = {
+ dirty: me.dirty,
+ data: Ext.apply({}, me.data),
+ generation: me.generation,
+ modified: modified && Ext.apply({}, modified),
+ previousValues: previousValues && Ext.apply({}, previousValues)
+ };
+ }
+ },
+ /**
+ * Calculate all summary fields on this record.
+ * @param {Ext.data.Model[]} records The records to use for calculation.
+ *
+ * @since 6.5.0
+ */
+ calculateSummary: function(records) {
+ var fields = this.getFields(),
+ len = fields.length,
+ recLen = records.length,
+ i, result, summary, prop, name, field;
+ for (i = 0; i < len; ++i) {
+ field = fields[i];
+ summary = field.getSummary();
+ if (summary) {
+ result = result || {};
+ name = field.name;
+ prop = field.summaryField || name;
+ result[name] = summary.calculate(records, prop, 'data', 0, recLen);
+ }
+ }
+ if (result) {
+ this.set(result, this._commitOptions);
+ }
+ },
+ /**
+ * Cancels all changes made in the current edit operation.
+ */
+ cancelEdit: function() {
+ var me = this,
+ editMemento = me.editMemento,
+ validation = me.validation;
+ if (editMemento) {
+ me.editing = false;
+ // reset the modified state, nothing changed since the edit began
+ Ext.apply(me, editMemento);
+ me.editMemento = null;
+ if (validation && validation.syncGeneration !== me.generation) {
+ validation.syncGeneration = 0;
+ }
+ }
+ },
+ /**
+ * Ends an edit. If any data was modified, the containing store is notified
+ * (ie, the store's `update` event will fire).
+ * @param {Boolean} [silent] True to not notify any stores of the change.
+ * @param {String[]} [modifiedFieldNames] Array of field names changed during edit.
+ */
+ endEdit: function(silent, modifiedFieldNames) {
+ var me = this,
+ editMemento = me.editMemento;
+ if (editMemento) {
+ me.editing = false;
+ me.editMemento = null;
+ // Since these reflect changes we never notified others about, the real set
+ // of "previousValues" is what we captured in the memento:
+ me.previousValues = editMemento.previousValues;
+ if (!silent) {
+ if (!modifiedFieldNames) {
+ modifiedFieldNames = me.getModifiedFieldNames(editMemento.data);
+ }
+ if (me.dirty || (modifiedFieldNames && modifiedFieldNames.length)) {
+ me.callJoined('afterEdit', [
+ modifiedFieldNames
+ ]);
+ }
+ }
+ }
+ },
+ getField: function(name) {
+ return this.self.getField(name);
+ },
+ /**
+ * Get the fields array for this model.
+ * @return {Ext.data.field.Field[]} The fields array
+ */
+ getFields: function() {
+ return this.self.getFields();
+ },
+ getFieldsMap: function() {
+ return this.fieldsMap;
+ },
+ /**
+ * Get the idProperty for this model.
+ * @return {String} The idProperty
+ */
+ getIdProperty: function() {
+ return this.idProperty;
+ },
+ /**
+ * Returns the unique ID allocated to this model instance as defined by `idProperty`.
+ * @return {Number/String} The id
+ */
+ getId: function() {
+ return this.id;
+ },
+ /**
+ * Return a unique observable ID. Model is not observable but tree nodes
+ * (`Ext.data.NodeInterface`) are, so they must be globally unique within the
+ * {@link #observableType}.
+ * @protected
+ */
+ getObservableId: function() {
+ return this.internalId;
+ },
+ /**
+ * Sets the model instance's id field to the given id.
+ * @param {Number/String} id The new id.
+ * @param {Object} [options] See {@link #set}.
+ */
+ setId: function(id, options) {
+ this.set(this.idProperty, id, options);
+ },
+ /**
+ * This method returns the value of a field given its name prior to its most recent
+ * change.
+ * @param {String} fieldName The field's {@link Ext.data.field.Field#name name}.
+ * @return {Object} The value of the given field prior to its current value. `undefined`
+ * if there is no previous value;
+ */
+ getPrevious: function(fieldName) {
+ var previousValues = this.previousValues;
+ return previousValues && previousValues[fieldName];
+ },
+ /**
+ * Returns true if the passed field name has been `{@link #modified}` since the load
+ * or last commit.
+ * @param {String} fieldName The field's {@link Ext.data.field.Field#name name}.
+ * @return {Boolean}
+ */
+ isModified: function(fieldName) {
+ var modified = this.modified;
+ return !!(modified && modified.hasOwnProperty(fieldName));
+ },
+ /**
+ * Returns the original value of a modified field. If there is no modified value,
+ * `undefined` will be return. Also see {@link #isModified}.
+ * @param {String} fieldName The name of the field for which to return the original value.
+ * @return {Object} modified
+ */
+ getModified: function(fieldName) {
+ var out;
+ if (this.isModified(fieldName)) {
+ out = this.modified[fieldName];
+ }
+ return out;
+ },
+ /**
+ * Returns the value of the given field.
+ * @param {String} fieldName The name of the field.
+ * @return {Object} The value of the specified field.
+ */
+ get: function(fieldName) {
+ return this.data[fieldName];
+ },
+ // This object is used whenever the set() method is called and given a string as the
+ // first argument. This approach saves memory (and GC costs) since we could be called
+ // a lot.
+ _singleProp: {},
+ _rejectOptions: {
+ convert: false,
+ silent: true
+ },
+ /**
+ * Sets the given field to the given value. For example:
+ *
+ * record.set('name', 'value');
+ *
+ * This method can also be passed an object containing multiple values to set at once.
+ * For example:
+ *
+ * record.set({
+ * name: 'value',
+ * age: 42
+ * });
+ *
+ * The following store events are fired when the modified record belongs to a store:
+ *
+ * - {@link Ext.data.Store#event-beginupdate beginupdate}
+ * - {@link Ext.data.Store#event-update update}
+ * - {@link Ext.data.Store#event-endupdate endupdate}
+ *
+ * @param {String/Object} fieldName The field to set, or an object containing key/value
+ * pairs.
+ * @param {Object} newValue The value for the field (if `fieldName` is a string).
+ * @param {Object} [options] Options for governing this update.
+ * @param {Boolean} [options.convert=true] Set to `false` to prevent any converters from
+ * being called during the set operation. This may be useful when setting a large bunch of
+ * raw values.
+ * @param {Boolean} [options.dirty=true] Pass `false` if the field values are to be
+ * understood as non-dirty (fresh from the server). When `true`, this change will be
+ * reflected in the `modified` collection.
+ * @param {Boolean} [options.commit=false] Pass `true` to call the {@link #commit} method
+ * after setting fields. If this option is passed, the usual after change processing will
+ * be bypassed. {@link #commit Commit} will be called even if there are no field changes.
+ * @param {Boolean} [options.silent=false] Pass `true` to suppress notification of any
+ * changes made by this call. Use with caution.
+ * @return {String[]} The array of modified field names or null if nothing was modified.
+ */
+ set: function(fieldName, newValue, options) {
+ var me = this,
+ cls = me.self,
+ data = me.data,
+ modified = me.modified,
+ prevVals = me.previousValues,
+ session = me.session,
+ single = Ext.isString(fieldName),
+ opt = (single ? options : newValue),
+ convertOnSet = opt ? opt.convert !== false : me.convertOnSet,
+ fieldsMap = me.fieldsMap,
+ silent = opt && opt.silent,
+ commit = opt && opt.commit,
+ updateRefs = !(opt && opt.refs === false) && session,
+ // Don't need to do dirty processing with commit, since we'll always
+ // end up with nothing modified and not dirty
+ dirty = !(opt && opt.dirty === false && !commit),
+ modifiedFieldNames = null,
+ dirtyRank = 0,
+ associations = me.associations,
+ currentValue, field, idChanged, key, name, oldId, comparator, dep, dependents, i, numFields, newId, rankedFields, reference, value, values, roleName;
+ if (single) {
+ values = me._singleProp;
+ values[fieldName] = newValue;
+ } else {
+ values = fieldName;
+ }
+ if (!(rankedFields = cls.rankedFields)) {
+ // On the first edit of a record of this type we need to ensure we have the
+ // topo-sort done:
+ rankedFields = cls.rankFields();
+ }
+ numFields = rankedFields.length;
+ do {
+ for (name in values) {
+ value = values[name];
+ currentValue = data[name];
+ comparator = me;
+ field = fieldsMap[name];
+ if (field) {
+ if (convertOnSet && field.convert) {
+ value = field.convert(value, me);
+ }
+ comparator = field;
+ reference = field.reference;
+ } else {
+ reference = null;
+ }
+ if (comparator.isEqual(currentValue, value)) {
+
+ continue;
+ }
+ data[name] = value;
+ (modifiedFieldNames || (modifiedFieldNames = [])).push(name);
+ (prevVals || (me.previousValues = prevVals = {}))[name] = currentValue;
+ if (reference && reference.cls) {
+ if (updateRefs) {
+ session.updateReference(me, field, value, currentValue);
+ }
+ reference.onValueChange(me, session, value, currentValue);
+ }
+ i = (dependents = field && field.dependents) && dependents.length;
+ while (i-- > 0) {
+ (dep = dependents[i]).dirty = true;
+ dirtyRank = dirtyRank ? Math.min(dirtyRank, dep.rank) : dep.rank;
+ }
+ if (!field || field.persist) {
+ if (modified && modified.hasOwnProperty(name)) {
+ if (!dirty || comparator.isEqual(modified[name], value)) {
+ delete modified[name];
+ me.dirty = -1;
+ }
+ } else if (dirty) {
+ if (!modified) {
+ me.modified = modified = {};
+ }
+ me.dirty = true;
+ modified[name] = currentValue;
+ }
+ }
+ if (name === me.idField.name) {
+ idChanged = true;
+ oldId = currentValue;
+ newId = value;
+ }
+ }
+ if (!dirtyRank) {
+ break;
+ }
+ field = rankedFields[dirtyRank - 1];
+ field.dirty = false;
+ if (single) {
+ delete values[fieldName];
+ } else {
+ values = me._singleProp;
+ single = true;
+ }
+ fieldName = field.name;
+ values[fieldName] = data[fieldName];
+ convertOnSet = true;
+ for (; dirtyRank < numFields; ++dirtyRank) {
+ if (rankedFields[dirtyRank].dirty) {
+ break;
+ }
+ }
+ if (dirtyRank < numFields) {
+ ++dirtyRank;
+ } else {
+ dirtyRank = 0;
+ }
+ } while (// new value is the same, so no change...
+ // We need the cls to be present because it means the association class is loaded,
+ // otherwise it could be pending.
+ // we use the field instance to hold the dirty bit to avoid any
+ // extra allocations... we'll clear this before we depart. We do
+ // this so we can perform the fewest recalculations possible as
+ // each dependent field only needs to be recalculated once.
+ // The original value in me.modified equals the new value, so
+ // the field is no longer modified:
+ // fix me.dirty later (still truthy)
+ // create only when needed
+ // Unless there are dependent fields to process we can break now. This is
+ // what will happen for all code pre-dating the depends or simply not
+ // using it, so it will add very little overhead when not used.
+ // dirtyRank has the minimum rank (a 1-based value) of any dependent field
+ // that needs recalculating due to changes above. The way we go about this
+ // is to use our helper object for processing single argument invocations
+ // to process just this one field. This is because the act of setting it
+ // may cause another field to be invalidated, so while we cannot know at
+ // this moment all the fields we need to recalculate, we know that only
+ // those following this field in rankedFields can possibly be among them.
+ // dirtyRank is 1-based
+ // clear just this field's dirty state
+ // cleanup last value
+ // switch over
+ // We are now processing a dependent field, so we want to force a
+ // convert to occur because it's the only way it will get a value
+ // Since dirtyRank is 1-based and refers to the field we need to handle
+ // on this pass, we can treat it like an index for a minute and look at
+ // the next field on towards the end to find the index of the next dirty
+ // field.
+ // We found a field after this one marked as dirty so make the index
+ // a proper 1-based rank:
+ // We did not find any more dirty fields after this one, so clear the
+ // dirtyRank and we will perhaps fall out after the next update
+ 1);
+ // eslint-disable-line no-constant-condition
+ if (me.dirty < 0) {
+ // We might have removed the last modified field, so check to see if there
+ // are any modified fields remaining and correct me.dirty:
+ me.dirty = false;
+ for (key in modified) {
+ if (modified.hasOwnProperty(key)) {
+ me.dirty = true;
+ break;
+ }
+ }
+ }
+ if (single) {
+ // cleanup our reused object for next time... important to do this before
+ // we fire any events or call anyone else (like afterEdit)!
+ delete values[fieldName];
+ }
+ ++me.generation;
+ if (idChanged) {
+ me.id = newId;
+ me.onIdChanged(newId, oldId);
+ me.callJoined('onIdChanged', [
+ oldId,
+ newId
+ ]);
+ if (associations) {
+ for (roleName in associations) {
+ associations[roleName].onIdChanged(me, oldId, newId);
+ }
+ }
+ }
+ if (commit) {
+ me.commit(silent, modifiedFieldNames);
+ } else if (!silent && !me.editing && modifiedFieldNames) {
+ me.callJoined('afterEdit', [
+ modifiedFieldNames
+ ]);
+ }
+ return modifiedFieldNames;
+ },
+ /**
+ * Usually called by the {@link Ext.data.Store} to which this model instance has been
+ * {@link #join joined}. Rejects all changes made to the model instance since either creation,
+ * or the last commit operation. Modified fields are reverted to their original values.
+ *
+ * Developers should subscribe to the {@link Ext.data.Store#event-update} event to have their
+ * code notified of reject operations.
+ *
+ * @param {Boolean} [silent=false] `true` to skip notification of the owning store of the
+ * change.
+ */
+ reject: function(silent) {
+ var me = this,
+ modified = me.modified;
+ if (me.erased) {
+ Ext.raise('Cannot reject once a record has been erased.');
+ }
+ if (modified) {
+ me.set(modified, me._rejectOptions);
+ }
+ me.dropped = false;
+ me.clearState();
+ if (!silent) {
+ me.callJoined('afterReject');
+ }
+ },
+ /**
+ * Usually called by the {@link Ext.data.Store} which owns the model instance. Commits all
+ * changes made to the instance since either creation or the last commit operation.
+ *
+ * Developers should subscribe to the {@link Ext.data.Store#event-update} event to have their
+ * code notified of commit operations.
+ *
+ * @param {Boolean} [silent=false] Pass `true` to skip notification of the owning store of the
+ * change.
+ * @param {String[]} [modifiedFieldNames] Array of field names changed during sync with server
+ * if known. Omit or pass `null` if unknown. An empty array means that it is known that
+ * no fields were modified by the server's response.
+ * Defaults to false.
+ */
+ commit: function(silent, modifiedFieldNames) {
+ var me = this,
+ versionProperty = me.versionProperty,
+ data = me.data,
+ erased;
+ me.clearState();
+ if (versionProperty && !me.phantom && !isNaN(data[versionProperty])) {
+ ++data[versionProperty];
+ }
+ me.phantom = false;
+ if (me.dropped) {
+ me.erased = erased = true;
+ }
+ if (!silent) {
+ if (erased) {
+ me.callJoined('afterErase');
+ } else {
+ me.callJoined('afterCommit', [
+ modifiedFieldNames
+ ]);
+ }
+ }
+ },
+ clearState: function() {
+ var me = this;
+ me.dirty = me.editing = false;
+ me.editMemento = me.modified = null;
+ },
+ /**
+ * Marks this record as `dropped` and waiting to be deleted on the server. When a
+ * record is dropped, it is automatically removed from all association stores and
+ * any child records associated to this record are also dropped (a "cascade delete")
+ * depending on the `cascade` parameter.
+ *
+ * @param {Boolean} [cascade=true] Pass `false` to disable the cascade to drop child
+ * records.
+ * @since 5.0.0
+ */
+ drop: function(cascade) {
+ var me = this,
+ associations = me.associations,
+ session = me.session,
+ roleName;
+ if (me.erased || me.dropped) {
+ return;
+ }
+ me.dropped = true;
+ if (associations && cascade !== false) {
+ for (roleName in associations) {
+ associations[roleName].onDrop(me, session);
+ }
+ }
+ me.callJoined('afterDrop');
+ if (me.phantom) {
+ me.setErased();
+ }
+ },
+ /**
+ * Tells this model instance that an observer is looking at it.
+ * @param {Ext.data.Store} owner The store or other owner object to which this model
+ * has been added.
+ */
+ join: function(owner) {
+ var me = this,
+ joined = me.joined;
+ // Optimize this, gets called a lot
+ if (!joined) {
+ joined = me.joined = [
+ owner
+ ];
+ } else if (!joined.length) {
+ joined[0] = owner;
+ } else {
+ // TODO: do we need joined here? Perhaps push will do.
+ Ext.Array.include(joined, owner);
+ }
+ if (owner.isStore && !me.store) {
+ /**
+ * @property {Ext.data.Store} store
+ * The {@link Ext.data.Store Store} to which this instance belongs.
+ *
+ * **Note:** If this instance is bound to multiple stores, this property
+ * will reference only the first.
+ */
+ me.store = owner;
+ }
+ },
+ /**
+ * Tells this model instance that it has been removed from the store.
+ * @param {Ext.data.Store} owner The store or other owner object from which this
+ * model has been removed.
+ */
+ unjoin: function(owner) {
+ var me = this,
+ joined = me.joined,
+ // TreeModels are never joined to their TreeStore.
+ // But unjoin is called by the base class's onCollectionRemove, so joined may be
+ // undefined.
+ len = joined && joined.length,
+ store = me.store,
+ i;
+ if (owner === me.session) {
+ me.session = null;
+ } else {
+ if (len === 1 && joined[0] === owner) {
+ joined.length = 0;
+ } else if (len) {
+ Ext.Array.remove(joined, owner);
+ }
+ if (store === owner) {
+ store = null;
+ if (joined) {
+ for (i = 0 , len = joined.length; i < len; ++i) {
+ owner = joined[i];
+ if (owner.isStore) {
+ store = owner;
+ break;
+ }
+ }
+ }
+ me.store = store;
+ }
+ }
+ },
+ /**
+ * Creates a clone of this record. States like `dropped`, `phantom` and `dirty` are
+ * all preserved in the cloned record.
+ *
+ * @param {Ext.data.Session} [session] The session to which the new record
+ * belongs.
+ * @return {Ext.data.Model} The cloned record.
+ */
+ clone: function(session) {
+ var me = this,
+ modified = me.modified,
+ ret = me.copy(me.id, session);
+ if (modified) {
+ // Restore the modified fields state
+ ret.modified = Ext.apply({}, modified);
+ }
+ ret.dirty = me.dirty;
+ ret.dropped = me.dropped;
+ ret.phantom = me.phantom;
+ return ret;
+ },
+ /**
+ * Creates a clean copy of this record. The returned record will not consider any its
+ * fields as modified.
+ *
+ * To generate a phantom instance with a new id pass `null`:
+ *
+ * var rec = record.copy(null); // clone the record but no id (one is generated)
+ *
+ * @param {String} [newId] A new id, defaults to the id of the instance being copied.
+ * See `{@link Ext.data.Model#idProperty idProperty}`.
+ * @param {Ext.data.Session} [session] The session to which the new record
+ * belongs.
+ *
+ * @return {Ext.data.Model}
+ */
+ copy: function(newId, session) {
+ var me = this,
+ data = Ext.apply({}, me.data),
+ idProperty = me.idProperty,
+ T = me.self;
+ if (newId || newId === 0) {
+ data[idProperty] = newId;
+ } else if (newId === null) {
+ delete data[idProperty];
+ }
+ return new T(data, session);
+ },
+ /**
+ * Returns the configured Proxy for this Model.
+ * @return {Ext.data.proxy.Proxy} The proxy
+ */
+ getProxy: function() {
+ return this.self.getProxy();
+ },
+ /**
+ * Returns the `Ext.data.Validation` record holding the results of this record's
+ * `validators`. This record is lazily created on first request and is then kept on
+ * this record to be updated later.
+ *
+ * See the class description for more about `validators`.
+ *
+ * @param {Boolean} [refresh] Pass `false` to not call the `refresh` method on the
+ * validation instance prior to returning it. Pass `true` to force a `refresh` of the
+ * validation instance. By default the returned record is only refreshed if changes
+ * have been made to this record.
+ * @return {Ext.data.Validation} The `Validation` record for this record.
+ * @since 5.0.0
+ */
+ getValidation: function(refresh) {
+ var me = this,
+ ret = me.validation;
+ if (!ret) {
+ me.validation = ret = new Ext.data.Validation();
+ ret.attach(me);
+ }
+ if (refresh === true || (refresh !== false && ret.syncGeneration !== me.generation)) {
+ ret.refresh(refresh);
+ }
+ return ret;
+ },
+ /**
+ * Validates the current data against all of its configured {@link #validators}. The
+ * returned collection holds an object for each reported problem from a `validator`.
+ *
+ * @return {Ext.data.ErrorCollection} The errors collection.
+ * @deprecated 5.0 Use `getValidation` instead.
+ */
+ validate: function() {
+ return new Ext.data.ErrorCollection().init(this);
+ },
+ /**
+ * Checks if the model is valid. See {@link #getValidation}.
+ * @return {Boolean} True if the model is valid.
+ */
+ isValid: function() {
+ return this.getValidation().isValid();
+ },
+ /**
+ * Returns a url-suitable string for this model instance. By default this just returns the
+ * name of the Model class followed by the instance ID - for example an instance of
+ * MyApp.model.User with ID 123 will return 'user/123'.
+ * @return {String} The url string for this model instance.
+ */
+ toUrl: function() {
+ var pieces = this.$className.split('.'),
+ name = pieces[pieces.length - 1].toLowerCase();
+ return name + '/' + this.getId();
+ },
+ /**
+ * @method erase
+ * @localdoc Destroys the model using the configured proxy. The erase action is
+ * asynchronous. Any processing of the erased record should be done in a callback.
+ *
+ * Ext.define('MyApp.model.User', {
+ * extend: 'Ext.data.Model',
+ * fields: [
+ * {name: 'id', type: 'int'},
+ * {name: 'name', type: 'string'}
+ * ],
+ * proxy: {
+ * type: 'ajax',
+ * url: 'server.url'
+ * }
+ * });
+ *
+ * var user = new MyApp.model.User({
+ * name: 'Foo'
+ * });
+ *
+ * // pass the phantom record data to the server to be saved
+ * user.save({
+ * success: function(record, operation) {
+ * // do something if the save succeeded
+ * // erase the created record
+ * record.erase({
+ * failure: function(record, operation) {
+ * // do something if the erase failed
+ * },
+ * success: function(record, operation) {
+ * // do something if the erase succeeded
+ * },
+ * callback: function(record, operation, success) {
+ * // do something if the erase succeeded or failed
+ * }
+ * });
+ * }
+ * });
+ *
+ * **NOTE:** If a {@link #phantom} record is erased it will not be processed via the
+ * proxy. However, any passed `success` or `callback` functions will be called.
+ *
+ * The options param is an {@link Ext.data.operation.Destroy} config object
+ * containing success, failure and callback functions, plus optional scope.
+ *
+ * @inheritdoc #method-load
+ * @return {Ext.data.operation.Destroy} The destroy operation
+ */
+ erase: function(options) {
+ var me = this;
+ me.erasing = true;
+ // Drop causes a removal from the backing Collection.
+ // The store's onCollectionRemove will respond to this by adding the record to
+ // its "to remove" stack and setting its needsSync
+ // flag unless the above "erasing" flag is set.
+ me.drop();
+ me.erasing = false;
+ return me.save(options);
+ },
+ setErased: function() {
+ this.erased = true;
+ this.callJoined('afterErase');
+ },
+ /**
+ * Gets an object of only the fields that have been modified since this record was
+ * created or committed. Only persistent fields are tracked in the `modified` set so
+ * this method will only return changes to persistent fields.
+ *
+ * For more control over the returned data, see `{@link #getData}`.
+ * @return {Object}
+ */
+ getChanges: function() {
+ return this.getData(this._getChangesOptions);
+ },
+ /**
+ * Returns the array of fields that are declared as critical (must always send).
+ * @return {Ext.data.field.Field[]}
+ */
+ getCriticalFields: function() {
+ var cls = this.self,
+ ret = cls.criticalFields;
+ if (!ret) {
+ cls.rankFields();
+ ret = cls.criticalFields;
+ }
+ return ret;
+ },
+ /**
+ * This method is called by the {@link Ext.data.reader.Reader} after loading a model from
+ * the server. This is after processing any inline associations that are available.
+ *
+ * @method onLoad
+ *
+ * @protected
+ * @template
+ */
+ /**
+ * Gets all of the data from this Models *loaded* associations. It does this
+ * recursively. For example if we have a User which hasMany Orders, and each Order
+ * hasMany OrderItems, it will return an object like this:
+ *
+ * {
+ * orders: [
+ * {
+ * id: 123,
+ * status: 'shipped',
+ * orderItems: [
+ * ...
+ * ]
+ * }
+ * ]
+ * }
+ *
+ * @param {Object} [result] The object on to which the associations will be added. If
+ * no object is passed one is created. This object is then returned.
+ * @param {Boolean/Object} [options] An object containing options describing the data
+ * desired.
+ * @param {Boolean} [options.associated=true] Pass `true` to include associated data from
+ * other associated records.
+ * @param {Boolean} [options.changes=false] Pass `true` to only include fields that
+ * have been modified. Note that field modifications are only tracked for fields that
+ * are not declared with `persist` set to `false`. In other words, only persistent
+ * fields have changes tracked so passing `true` for this means `options.persist` is
+ * redundant.
+ * @param {Boolean} [options.critical] Pass `true` to include fields set as `critical`.
+ * This is only meaningful when `options.changes` is `true` since critical fields may
+ * not have been modified.
+ * @param {Boolean} [options.persist] Pass `true` to only return persistent fields.
+ * This is implied when `options.changes` is set to `true`.
+ * @param {Boolean} [options.serialize=false] Pass `true` to invoke the `serialize`
+ * method on the returned fields.
+ * @return {Object} The nested data set for the Model's loaded associations.
+ */
+ getAssociatedData: function(result, options) {
+ var me = this,
+ associations = me.associations,
+ deep, i, item, items, itemData, length, record, role, roleName, opts, clear, associated;
+ result = result || {};
+ me.$gathering = 1;
+ if (options) {
+ options = Ext.apply({}, options);
+ }
+ for (roleName in associations) {
+ role = associations[roleName];
+ item = role.getAssociatedItem(me);
+ if (!item || item.$gathering) {
+
+ continue;
+ }
+ if (item.isStore) {
+ item.$gathering = 1;
+ items = item.getData().items;
+ // get the records for the store
+ length = items.length;
+ itemData = [];
+ for (i = 0; i < length; ++i) {
+ // NOTE - we don't check whether the record is gathering here because
+ // we cannot remove it from the store (it would invalidate the index
+ // values and misrepresent the content). Instead we tell getData to
+ // only get the fields vs descend further.
+ record = items[i];
+ deep = !record.$gathering;
+ record.$gathering = 1;
+ if (options) {
+ associated = options.associated;
+ if (associated === undefined) {
+ options.associated = deep;
+ clear = true;
+ } else if (!deep) {
+ options.associated = false;
+ clear = true;
+ }
+ opts = options;
+ } else {
+ opts = deep ? me._getAssociatedOptions : me._getNotAssociatedOptions;
+ }
+ itemData.push(record.getData(opts));
+ if (clear) {
+ options.associated = associated;
+ clear = false;
+ }
+ delete record.$gathering;
+ }
+ delete item.$gathering;
+ } else {
+ opts = options || me._getAssociatedOptions;
+ if (options && options.associated === undefined) {
+ opts.associated = true;
+ }
+ itemData = item.getData(opts);
+ }
+ result[roleName] = itemData;
+ }
+ delete me.$gathering;
+ return result;
+ },
+ /**
+ * Gets all values for each field in this model and returns an object containing the
+ * current data. This can be tuned by passing an `options` object with various
+ * properties describing the desired result. Passing `true` simply returns all fields
+ * *and* all associated record data.
+ *
+ * To selectively gather some associated data, the `options` object can be used as
+ * follows:
+ *
+ * var data = order.getData({
+ * associated: {
+ * orderItems: true
+ * }
+ * });
+ *
+ * This will include all data fields as well as an "orderItems" array with the data
+ * for each `OrderItem`. To include the associated `Item` for each `OrderItem`, the
+ * call would look like:
+ *
+ * var data = order.getData({
+ * associated: {
+ * orderItems: {
+ * item: true
+ * }
+ * }
+ * });
+ *
+ * @param {Boolean/Object} [options] An object containing options describing the data
+ * desired. If `true` is passed it is treated as an object with `associated` set to
+ * `true`.
+ * @param {Boolean/Object} [options.associated=false] Pass `true` to recursively
+ * include all associated data. This is equivalent to pass `true` as the only argument.
+ * See `getAssociatedData`. If `associated` is an object, it describes the specific
+ * associations to gather.
+ * @param {Boolean} [options.changes=false] Pass `true` to only include fields that
+ * have been modified. Note that field modifications are only tracked for fields that
+ * are not declared with `persist` set to `false`. In other words, only persistent
+ * fields have changes tracked so passing `true` for this means `options.persist` is
+ * redundant.
+ * @param {Boolean} [options.critical] Pass `true` to include fields set as `critical`.
+ * This is only meaningful when `options.changes` is `true` since critical fields may
+ * not have been modified.
+ * @param {Boolean} [options.persist] Pass `true` to only return persistent fields.
+ * This is implied when `options.changes` is set to `true`.
+ * @param {Boolean} [options.serialize=false] Pass `true` to invoke the `serialize`
+ * method on the returned fields.
+ * @return {Object} An object containing all the values in this model.
+ */
+ getData: function(options) {
+ var me = this,
+ ret = {},
+ opts = (options === true) ? me._getAssociatedOptions : (options || ret),
+ // cheat
+ data = me.data,
+ associated = opts.associated,
+ changes = opts.changes,
+ critical = changes && opts.critical,
+ content = changes ? me.modified : data,
+ fieldsMap = me.fieldsMap,
+ persist = opts.persist,
+ serialize = opts.serialize,
+ criticalFields, field, n, name, value;
+ // DON'T use "opts" from here on...
+ // Keep in mind the two legacy use cases:
+ // - getData() ==> Ext.apply({}, me.data)
+ // - getData(true) ==> Ext.apply(Ext.apply({}, me.data), me.getAssociatedData())
+ if (content) {
+ // when processing only changes, me.modified could be null
+ for (name in content) {
+ value = data[name];
+ field = fieldsMap[name];
+ if (field) {
+ if (persist && !field.persist) {
+
+ continue;
+ }
+ if (serialize && field.serialize) {
+ value = field.serialize(value, me);
+ }
+ }
+ ret[name] = value;
+ }
+ }
+ if (critical) {
+ criticalFields = me.self.criticalFields || me.getCriticalFields();
+ for (n = criticalFields.length; n-- > 0; ) {
+ name = (field = criticalFields[n]).name;
+ if (!(name in ret)) {
+ value = data[name];
+ if (serialize && field.serialize) {
+ value = field.serialize(value, me);
+ }
+ ret[name] = value;
+ }
+ }
+ }
+ if (associated) {
+ if (typeof associated === 'object') {
+ me.getNestedData(opts, ret);
+ } else {
+ me.getAssociatedData(ret, opts);
+ }
+ }
+ return ret;
+ },
+ getNestedData: function(options, result) {
+ var me = this,
+ associations = me.associations,
+ graph = options.associated,
+ i, item, items, itemData, length, record, role, roleName, opts;
+ result = result || {};
+ // For example:
+ //
+ // associated: {
+ // orderItems: true
+ // }
+ //
+ // associated: {
+ // orderItems: {
+ // item: true
+ // }
+ // }
+ //
+ for (roleName in graph) {
+ role = associations[roleName];
+ opts = graph[roleName];
+ if (opts === true) {
+ delete options.associated;
+ } else {
+ options.associated = opts;
+ }
+ item = role.getAssociatedItem(me);
+ if (item.isStore) {
+ items = item.getData().items;
+ // get the records for the store
+ length = items.length;
+ itemData = [];
+ for (i = 0; i < length; ++i) {
+ record = items[i];
+ itemData.push(record.getData(options));
+ }
+ } else {
+ itemData = item.getData(options);
+ }
+ result[roleName] = itemData;
+ }
+ options.associated = graph;
+ // restore the original value
+ return result;
+ },
+ /**
+ * Returns the array of fields that are declared as non-persist or "transient".
+ * @return {Ext.data.field.Field[]}
+ * @since 5.0.0
+ */
+ getTransientFields: function() {
+ var cls = this.self,
+ ret = cls.transientFields;
+ if (!ret) {
+ cls.rankFields();
+ // populates transientFields as well as rank
+ ret = cls.transientFields;
+ }
+ return ret;
+ },
+ /**
+ * Checks whether this model is loading data from the {@link #proxy}.
+ * @return {Boolean} `true` if in a loading state.
+ */
+ isLoading: function() {
+ return !!this.loadOperation;
+ },
+ /**
+ * Aborts a pending {@link #method!load} operation. If the record is not loading, this does
+ * nothing.
+ */
+ abort: function() {
+ var operation = this.loadOperation;
+ if (operation) {
+ operation.abort();
+ }
+ },
+ /**
+ * @localdoc Loads the model instance using the configured proxy. The load action
+ * is asynchronous. Any processing of the loaded record should be done in a
+ * callback.
+ *
+ * Ext.define('MyApp.model.User', {
+ * extend: 'Ext.data.Model',
+ * fields: [
+ * {name: 'id', type: 'int'},
+ * {name: 'name', type: 'string'}
+ * ],
+ * proxy: {
+ * type: 'ajax',
+ * url: 'server.url'
+ * }
+ * });
+ *
+ * var user = new MyApp.model.User();
+ * user.load({
+ * scope: this,
+ * failure: function(record, operation) {
+ * // do something if the load failed
+ * },
+ * success: function(record, operation) {
+ * // do something if the load succeeded
+ * },
+ * callback: function(record, operation, success) {
+ * // do something whether the load succeeded or failed
+ * }
+ * });
+ *
+ * The options param is an {@link Ext.data.operation.Read} config object containing
+ * success, failure and callback functions, plus optional scope.
+ *
+ * @param {Object} [options] Options to pass to the proxy.
+ * @param {Function} options.success A function to be called when the
+ * model is processed by the proxy successfully.
+ * The callback is passed the following parameters:
+ * @param {Ext.data.Model} options.success.record The record.
+ * @param {Ext.data.operation.Operation} options.success.operation The operation.
+ *
+ * @param {Function} options.failure A function to be called when the
+ * model is unable to be processed by the server.
+ * The callback is passed the following parameters:
+ * @param {Ext.data.Model} options.failure.record The record.
+ * @param {Ext.data.operation.Operation} options.failure.operation The operation.
+ *
+ * @param {Function} options.callback A function to be called whether the proxy
+ * transaction was successful or not.
+ * The callback is passed the following parameters:
+ * @param {Ext.data.Model} options.callback.record The record.
+ * @param {Ext.data.operation.Operation} options.callback.operation The operation.
+ * @param {Boolean} options.callback.success `true` if the operation was successful.
+ *
+ * @param {Object} options.scope The scope in which to execute the callback
+ * functions. Defaults to the model instance.
+ *
+ * @return {Ext.data.operation.Read} The read operation.
+ */
+ load: function(options) {
+ options = Ext.apply({}, options);
+ /* eslint-disable-next-line vars-on-top */
+ var me = this,
+ scope = options.scope || me,
+ proxy = me.getProxy(),
+ callback = options.callback,
+ operation = me.loadOperation,
+ id = me.getId(),
+ extras;
+ if (operation) {
+ // Already loading, push any callbacks on and jump out
+ extras = operation.extraCalls;
+ if (!extras) {
+ extras = operation.extraCalls = [];
+ }
+ extras.push(options);
+ return operation;
+ }
+ var doIdCheck = true;
+ // eslint-disable-line vars-on-top, one-var
+ if (me.phantom) {
+ doIdCheck = false;
+ }
+ options.id = id;
+ // Always set the recordCreator. If we have a session, we're already
+ // part of said session, so we don't need to handle that.
+ options.recordCreator = function(data, type, readOptions) {
+ // Important to change this here, because we might be loading associations,
+ // so we do not want this to propagate down. If we have a session, use that
+ // so that we end up getting the same record. Otherwise, just remove it.
+ var session = me.session;
+ if (readOptions) {
+ readOptions.recordCreator = session ? session.recordCreator : null;
+ }
+ me.set(data, me._commitOptions);
+ // Do the id check after set since converters may have run
+ if (doIdCheck && me.getId() !== id) {
+ Ext.raise('Invalid record id returned for ' + id + '@' + me.entityName);
+ }
+ return me;
+ };
+ options.internalCallback = function(operation) {
+ var success = operation.wasSuccessful() && operation.getRecords().length > 0,
+ op = me.loadOperation,
+ extras = op.extraCalls,
+ successFailArgs = [
+ me,
+ operation
+ ],
+ callbackArgs = [
+ me,
+ operation,
+ success
+ ],
+ i, len;
+ me.loadOperation = null;
+ ++me.loadCount;
+ if (success) {
+ Ext.callback(options.success, scope, successFailArgs);
+ } else {
+ Ext.callback(options.failure, scope, successFailArgs);
+ }
+ Ext.callback(callback, scope, callbackArgs);
+ // Some code repetition here, however in a vast majority of cases
+ // we'll only have a single callback, so optimize for that case rather
+ // than setup arrays for all the callback options
+ if (extras) {
+ for (i = 0 , len = extras.length; i < len; ++i) {
+ options = extras[i];
+ if (success) {
+ Ext.callback(options.success, scope, successFailArgs);
+ } else {
+ Ext.callback(options.failure, scope, successFailArgs);
+ }
+ Ext.callback(options.callback, scope, callbackArgs);
+ }
+ }
+ me.callJoined('afterLoad');
+ };
+ delete options.callback;
+ me.loadOperation = operation = proxy.createOperation('read', options);
+ operation.execute();
+ return operation;
+ },
+ /**
+ * Merge incoming data from the server when this record exists
+ * in an active session. This method is not called if this record is
+ * loaded directly via {@link #method!load}. The default behaviour is to use incoming
+ * data if the record is not {@link #dirty}, otherwise the data is
+ * discarded. This method should be overridden in subclasses to
+ * provide a different behavior.
+ * @param {Object} data The model data retrieved from the server.
+ *
+ * @protected
+ *
+ * @since 6.5.0
+ */
+ mergeData: function(data) {
+ if (!this.dirty) {
+ this.set(data, this._commitOptions);
+ }
+ },
+ /**
+ * @method save
+ * @localdoc Saves the model instance using the configured proxy. The save action
+ * is asynchronous. Any processing of the saved record should be done in a callback.
+ *
+ * Create example:
+ *
+ * Ext.define('MyApp.model.User', {
+ * extend: 'Ext.data.Model',
+ * fields: [
+ * {name: 'id', type: 'int'},
+ * {name: 'name', type: 'string'}
+ * ],
+ * proxy: {
+ * type: 'ajax',
+ * url: 'server.url'
+ * }
+ * });
+ *
+ * var user = new MyApp.model.User({
+ * name: 'Foo'
+ * });
+ *
+ * // pass the phantom record data to the server to be saved
+ * user.save({
+ * failure: function(record, operation) {
+ * // do something if the save failed
+ * },
+ * success: function(record, operation) {
+ * // do something if the save succeeded
+ * },
+ * callback: function(record, operation, success) {
+ * // do something whether the save succeeded or failed
+ * }
+ * });
+ *
+ * The response from a create operation should include the ID for the newly created
+ * record:
+ *
+ * // sample response
+ * {
+ * success: true,
+ * id: 1
+ * }
+ *
+ * // the id may be nested if the proxy's reader has a rootProperty config
+ * Ext.define('MyApp.model.User', {
+ * extend: 'Ext.data.Model',
+ * proxy: {
+ * type: 'ajax',
+ * url: 'server.url',
+ * reader: {
+ * type: 'ajax',
+ * rootProperty: 'data'
+ * }
+ * }
+ * });
+ *
+ * // sample nested response
+ * {
+ * success: true,
+ * data: {
+ * id: 1
+ * }
+ * }
+ *
+ * (Create + ) Update example:
+ *
+ * Ext.define('MyApp.model.User', {
+ * extend: 'Ext.data.Model',
+ * fields: [
+ * {name: 'id', type: 'int'},
+ * {name: 'name', type: 'string'}
+ * ],
+ * proxy: {
+ * type: 'ajax',
+ * url: 'server.url'
+ * }
+ * });
+ *
+ * var user = new MyApp.model.User({
+ * name: 'Foo'
+ * });
+ * user.save({
+ * success: function(record, operation) {
+ * record.set('name', 'Bar');
+ * // updates the remote record via the proxy
+ * record.save();
+ * }
+ * });
+ *
+ * (Create + ) Destroy example - see also {@link #erase}:
+ *
+ * Ext.define('MyApp.model.User', {
+ * extend: 'Ext.data.Model',
+ * fields: [
+ * {name: 'id', type: 'int'},
+ * {name: 'name', type: 'string'}
+ * ],
+ * proxy: {
+ * type: 'ajax',
+ * url: 'server.url'
+ * }
+ * });
+ *
+ * var user = new MyApp.model.User({
+ * name: 'Foo'
+ * });
+ * user.save({
+ * success: function(record, operation) {
+ * record.drop();
+ * // destroys the remote record via the proxy
+ * record.save();
+ * }
+ * });
+ *
+ * **NOTE:** If a {@link #phantom} record is {@link #drop dropped} and subsequently
+ * saved it will not be processed via the proxy. However, any passed `success`
+ * or `callback` functions will be called.
+ *
+ * The options param is an Operation config object containing success, failure and
+ * callback functions, plus optional scope. The type of Operation depends on the
+ * state of the model being saved.
+ *
+ * - {@link #phantom} model - {@link Ext.data.operation.Create}
+ * - {@link #isModified modified} model - {@link Ext.data.operation.Update}
+ * - {@link #dropped} model - {@link Ext.data.operation.Destroy}
+ *
+ * @inheritdoc #method-load
+ * @return {Ext.data.operation.Create/Ext.data.operation.Update/Ext.data.operation.Destroy}
+ * The operation instance for saving this model. The type of operation returned
+ * depends on the model state at the time of the action.
+ *
+ * - {@link #phantom} model - {@link Ext.data.operation.Create}
+ * - {@link #isModified modified} model - {@link Ext.data.operation.Update}
+ * - {@link #dropped} model - {@link Ext.data.operation.Destroy}
+ */
+ save: function(options) {
+ options = Ext.apply({}, options);
+ /* eslint-disable-next-line vars-on-top */
+ var me = this,
+ phantom = me.phantom,
+ dropped = me.dropped,
+ action = dropped ? 'destroy' : (phantom ? 'create' : 'update'),
+ scope = options.scope || me,
+ callback = options.callback,
+ proxy = me.getProxy(),
+ operation;
+ options.records = [
+ me
+ ];
+ options.internalCallback = function(operation) {
+ var args = [
+ me,
+ operation
+ ],
+ success = operation.wasSuccessful();
+ if (success) {
+ Ext.callback(options.success, scope, args);
+ } else {
+ Ext.callback(options.failure, scope, args);
+ }
+ args.push(success);
+ Ext.callback(callback, scope, args);
+ };
+ delete options.callback;
+ operation = proxy.createOperation(action, options);
+ // Not a phantom, then we must perform this operation on the remote datasource.
+ // Record will be removed from the store in the callback upon a success response
+ if (dropped && phantom) {
+ // If it's a phantom, then call the callback directly with a dummy successful ResultSet
+ operation.setResultSet(Ext.data.reader.Reader.prototype.nullResultSet);
+ me.setErased();
+ operation.setSuccessful(true);
+ } else {
+ operation.execute();
+ }
+ return operation;
+ },
+ //-------------------------------------------------------------------------
+ // Statics
+ statics: {
+ /**
+ * @property {String/Object}
+ * The default proxy to use for instances of this Model when no proxy is configured
+ * on the instance. When specified, the model will use this proxy instead of
+ * requesting one from the {@link Ext.data.Session Session}.
+ *
+ * Can be a string "type", or a {@link Ext.data.proxy.Proxy Proxy} config object.
+ *
+ * This proxy is not inherited by subclasses.
+ * @static
+ * @protected
+ */
+ defaultProxy: 'memory'
+ },
+ inheritableStatics: {
+ /**
+ * @property {Object} _associatedReadOptions
+ * The options for the proxy reader for loadData.
+ *
+ * @private
+ */
+ _associatedReadOptions: {
+ recordsOnly: true,
+ asRoot: true
+ },
+ /**
+ * Create a model while also parsing any data for associations.
+ * @param {Object} data The model data, including any associated data if required.
+ * The type of data should correspond to what the configured data reader would expect.
+ * @param {Ext.data.Session} [session] The session.
+ * @return {Ext.data.Model} The model.
+ *
+ * @static
+ * @inheritable
+ * @since 6.5.0
+ */
+ loadData: function(data, session) {
+ var rec;
+ if (data) {
+ /* eslint-disable-next-line max-len, newline-per-chained-call */
+ rec = this.getProxy().getReader().readRecords([
+ data
+ ], session ? {
+ recordCreator: session.recordCreator
+ } : undefined, this._associatedReadOptions)[0];
+ } else {
+ rec = new this(data, session);
+ }
+ return rec;
+ },
+ /**
+ * Get the summary model type. If {@link #summary} is specified, it is
+ * a new type that extends from this type. If not, then it is the same
+ * model type.
+ * @return {Ext.Class} The summary model type.
+ *
+ * @static
+ * @inheritable
+ * @since 6.5.0
+ */
+ getSummaryModel: function() {
+ var me = this,
+ proto = me.prototype,
+ summaryModel = me.summaryModel;
+ if (!summaryModel) {
+ summaryModel = Ext.define(null, {
+ extend: me,
+ fields: proto.summaryFields || [],
+ isSummaryModel: true
+ });
+ summaryModel.isSummaryModel = true;
+ me.summaryModel = proto.summaryModel = summaryModel;
+ }
+ return summaryModel || null;
+ },
+ /**
+ * This method adds the given set of fields to this model class.
+ *
+ * @param {String[]/Object[]} newFields The new fields to add. Based on the `name`
+ * of a field this may replace a previous field definition.
+ *
+ * @protected
+ * @static
+ * @inheritable
+ * @since 5.0.0
+ */
+ addFields: function(newFields) {
+ this.replaceFields(newFields);
+ },
+ /**
+ * This method replaces the specified set of fields with a given set of new fields.
+ * Fields should normally be considered immutable, but if the timing is right (that
+ * is, before derived classes are declared), it is permissible to change the fields
+ * collection.
+ *
+ * @param {String[]/Object[]} newFields The new fields to add. Based on the `name`
+ * of a field this may replace a previous field definition.
+ * @param {Boolean/String[]} removeFields The names of fields to remove or `true`
+ * to remove all existing fields. Removes are processed first followed by adds so
+ * if a field name appears in `newFields` as well that field will effectively be
+ * added (however, in that case there is no need to include the field in this
+ * array).
+ *
+ * @protected
+ * @static
+ * @inheritable
+ * @since 5.0.0
+ */
+ replaceFields: function(newFields, removeFields) {
+ var me = this,
+ proto = me.prototype,
+ Field = Ext.data.field.Field,
+ fields = me.fields,
+ fieldsMap = me.fieldsMap,
+ ordinals = me.fieldOrdinals,
+ field, i, idField, len, name, ordinal, cleared;
+ if (removeFields === true) {
+ fields.length = 0;
+ me.fieldsMap = fieldsMap = {};
+ me.fieldOrdinals = ordinals = {};
+ cleared = true;
+ } else if (removeFields) {
+ for (i = removeFields.length; i-- > 0; ) {
+ name = removeFields[i];
+ if (name in ordinals) {
+ delete ordinals[name];
+ delete fieldsMap[name];
+ }
+ }
+ for (i = 0 , len = fields.length; i < len; ++i) {
+ name = (field = fields[i]).name;
+ if (name in ordinals) {
+ ordinals[name] = i;
+ } else {
+ // This field is being removed (it is no longer in ordinals).
+ fields.splice(i, 1);
+ --i;
+ --len;
+ }
+ }
+ }
+ // we need to do this forwards so that ordinals don't become
+ // invalid due to a splice
+ for (i = 0 , len = newFields ? newFields.length : 0; i < len; i++) {
+ name = (field = newFields[i]).name;
+ if (!(name in ordinals)) {
+ ordinals[name] = ordinal = fields.length;
+ // 0-based
+ fields.push(field = Field.create(field));
+ fieldsMap[name] = field;
+ field.ordinal = ordinal;
+ field.definedBy = field.owner = this;
+ }
+ }
+ // Ext.data.NodeInterface
+ // Reset all ranks if we didn't get cleared, since this could
+ // alter the dependencies
+ if (!cleared) {
+ for (i = 0 , len = fields.length; i < len; ++i) {
+ fields[i].rank = null;
+ }
+ }
+ // The idField could have been replaced, so reacquire it.
+ me.idField = proto.idField = idField = fieldsMap[proto.idProperty];
+ if (idField) {
+ idField.allowNull = idField.critical = idField.identifier = true;
+ idField.defaultValue = null;
+ }
+ // In case we've created the initializer we need to zap it so we recreate it
+ // next time. Likewise with field ranking.
+ me.initializeFn = me.rankedFields = me.transientFields = me.criticalFields = null;
+ },
+ /**
+ * Removes the given set of fields from this model.
+ *
+ * @param {Boolean/String[]} removeFields The names of fields to remove or `true`
+ * to remove all existing fields. Removes are processed first followed by adds so
+ * if a field name appears in `newFields` as well that field will effectively be
+ * added (however, in that case there is no need to include the field in this
+ * array).
+ *
+ * @protected
+ * @static
+ * @inheritable
+ * @since 5.0.0
+ */
+ removeFields: function(removeFields) {
+ this.replaceFields(null, removeFields);
+ },
+ /**
+ * @private
+ * @static
+ * @inheritable
+ */
+ getIdFromData: function(data) {
+ var T = this,
+ idField = T.idField,
+ id = idField.calculated ? (new T(data)).id : data[idField.name];
+ return id;
+ },
+ /**
+ * @private
+ * @static
+ * @inheritable
+ */
+ createWithId: function(id, data, session) {
+ var d = data,
+ T = this;
+ if (id || id === 0) {
+ d = {};
+ if (data) {
+ Ext.apply(d, data);
+ }
+ d[T.idField.name] = id;
+ }
+ return new T(d, session);
+ },
+ /**
+ * @private
+ * @static
+ * @inheritable
+ */
+ getFields: function() {
+ return this.fields;
+ },
+ /**
+ * @private
+ * @static
+ * @inheritable
+ */
+ getFieldsMap: function() {
+ return this.fieldsMap;
+ },
+ /**
+ * @private
+ * @static
+ * @inheritable
+ */
+ getField: function(name) {
+ return this.fieldsMap[name] || null;
+ },
+ /**
+ * Returns the configured Proxy for this Model.
+ * @return {Ext.data.proxy.Proxy} The proxy
+ * @static
+ * @inheritable
+ */
+ getProxy: function() {
+ var me = this,
+ proxy = me.proxy,
+ defaultProxy = me.defaultProxy,
+ defaults;
+ if (!proxy) {
+ // Check what was defined by the class (via onClassExtended):
+ proxy = me.proxyConfig;
+ if (!proxy && defaultProxy) {
+ proxy = defaultProxy;
+ }
+ if (!proxy || !proxy.isProxy) {
+ if (typeof proxy === 'string') {
+ proxy = {
+ type: proxy
+ };
+ }
+ // We have nothing or a config for the proxy. Get some defaults from
+ // the Schema and smash anything we've provided over the top.
+ defaults = Ext.merge(me.schema.constructProxy(me), proxy);
+ if (proxy && proxy.type) {
+ proxy = proxy.schema === false ? proxy : defaults;
+ } else {
+ proxy = defaults;
+ }
+ }
+ proxy = me.setProxy(proxy);
+ }
+ return proxy;
+ },
+ /**
+ * Sets the Proxy to use for this model. Accepts any options that can be accepted by
+ * {@link Ext#createByAlias Ext.createByAlias}.
+ * @param {String/Object/Ext.data.proxy.Proxy} proxy The proxy
+ * @return {Ext.data.proxy.Proxy}
+ * @static
+ * @inheritable
+ */
+ setProxy: function(proxy) {
+ var me = this,
+ model;
+ if (proxy) {
+ if (!proxy.isProxy) {
+ proxy = Ext.Factory.proxy(proxy);
+ } else {
+ model = proxy.getModel();
+ if (model && model !== me) {
+ proxy = proxy.clone();
+ }
+ }
+ proxy.setModel(me);
+ }
+ return (me.prototype.proxy = me.proxy = proxy);
+ },
+ /**
+ * Asynchronously loads a model instance by id. Any processing of the loaded
+ * record should be done in a callback.
+ *
+ * Sample usage:
+ *
+ * Ext.define('MyApp.User', {
+ * extend: 'Ext.data.Model',
+ * fields: [
+ * {name: 'id', type: 'int'},
+ * {name: 'name', type: 'string'}
+ * ]
+ * });
+ *
+ * MyApp.User.load(10, {
+ * scope: this,
+ * failure: function(record, operation) {
+ * //do something if the load failed
+ * },
+ * success: function(record, operation) {
+ * //do something if the load succeeded
+ * },
+ * callback: function(record, operation, success) {
+ * //do something whether the load succeeded or failed
+ * }
+ * });
+ *
+ * @param {Number/String} id The ID of the model to load.
+ * **NOTE:** The model returned must have an ID matching the param in the load
+ * request.
+ *
+ * @param {Object} [options] The options param is an
+ * {@link Ext.data.operation.Read} config object containing success, failure and
+ * callback functions, plus optional scope.
+ *
+ * @param {Function} options.success A function to be called when the
+ * model is processed by the proxy successfully.
+ * The callback is passed the following parameters:
+ * @param {Ext.data.Model} options.success.record The record.
+ * @param {Ext.data.operation.Operation} options.success.operation The operation.
+ *
+ * @param {Function} options.failure A function to be called when the
+ * model is unable to be processed by the server.
+ * The callback is passed the following parameters:
+ * @param {Ext.data.Model} options.failure.record The record.
+ * @param {Ext.data.operation.Operation} options.failure.operation The operation.
+ *
+ * @param {Function} options.callback A function to be called whether the proxy
+ * transaction was successful or not.
+ * The callback is passed the following parameters:
+ * @param {Ext.data.Model} options.callback.record The record.
+ * @param {Ext.data.operation.Operation} options.callback.operation The
+ * operation.
+ * @param {Boolean} options.callback.success `true` if the operation was
+ * successful.
+ *
+ * @param {Object} options.scope The scope in which to execute the callback
+ * functions. Defaults to the model instance.
+ *
+ * @param {Ext.data.Session} [session] The session for this record.
+ *
+ * @return {Ext.data.Model} The newly created model. Note that the model will
+ * (probably) still be loading once it is returned from this method. To do any
+ * post-processing on the data, the appropriate place to do see is in the
+ * callback.
+ *
+ * @static
+ * @inheritable
+ */
+ load: function(id, options, session) {
+ var data = {},
+ rec;
+ if (session) {
+ rec = session.peekRecord(this, id);
+ }
+ if (!rec) {
+ data[this.prototype.idProperty] = id;
+ rec = new this(data, session);
+ }
+ rec.load(options);
+ return rec;
+ }
+ },
+ deprecated: {
+ 5: {
+ methods: {
+ hasId: null,
+ markDirty: null,
+ setDirty: null,
+ eachStore: function(callback, scope) {
+ var me = this,
+ stores = me.stores,
+ len = stores.length,
+ i;
+ for (i = 0; i < len; ++i) {
+ callback.call(scope, stores[i]);
+ }
+ },
+ join: function(item) {
+ var me = this,
+ stores = me.stores,
+ joined = me.joined;
+ if (!joined) {
+ joined = me.joined = [
+ item
+ ];
+ } else {
+ joined.push(item);
+ }
+ if (item.isStore) {
+ me.store = me.store || item;
+ if (!stores) {
+ stores = me.stores = [];
+ }
+ stores.push(item);
+ }
+ },
+ unjoin: function(item) {
+ var me = this,
+ stores = me.stores,
+ joined = me.joined;
+ if (joined.length === 1) {
+ joined.length = 0;
+ } else {
+ Ext.Array.remove(joined, item);
+ }
+ if (item.isStore) {
+ Ext.Array.remove(stores, item);
+ me.store = stores[0] || null;
+ }
+ }
+ },
+ properties: {
+ persistenceProperty: null
+ },
+ inheritableStatics: {
+ methods: {
+ setFields: null
+ }
+ }
+ }
+ },
+ //-------------------------------------------------------------------------
+ privates: {
+ _commitOptions: {
+ commit: true
+ },
+ _getChangesOptions: {
+ changes: true
+ },
+ _getAssociatedOptions: {
+ associated: true
+ },
+ _getNotAssociatedOptions: {
+ associated: false
+ },
+ _metaProperties: {
+ dirty: 'isDirty',
+ phantom: 'isPhantom',
+ valid: 'isValid'
+ },
+ /**
+ * Copies data from the passed record into this record. If the passed record is undefined,
+ * does nothing.
+ *
+ * If this is a phantom record (represented only in the client, with no corresponding
+ * database entry), and the source record is not a phantom, then this record acquires
+ * the id of the source record.
+ *
+ * @param {Ext.data.Model} sourceRecord The record to copy data from.
+ * @return {String[]} The names of the fields which changed value.
+ * @private
+ */
+ copyFrom: function(sourceRecord) {
+ var me = this,
+ fields = me.fields,
+ fieldCount = fields.length,
+ modifiedFieldNames = [],
+ idProperty = me.idProperty,
+ i = 0,
+ field, myData, sourceData, name, value;
+ if (sourceRecord) {
+ myData = me.data;
+ sourceData = sourceRecord.data;
+ for (; i < fieldCount; i++) {
+ field = fields[i];
+ name = field.name;
+ // Do not use setters.
+ // Copy returned values in directly from the data object.
+ // Converters have already been called because new Records
+ // have been created to copy from.
+ // This is a direct record-to-record value copy operation.
+ // don't copy the id, we'll do it at the end
+ if (name !== idProperty) {
+ value = sourceData[name];
+ // If source property is specified, and value is different
+ // copy field value in and build updatedFields
+ if (value !== undefined && !me.isEqual(myData[name], value)) {
+ myData[name] = value;
+ modifiedFieldNames.push(name);
+ }
+ }
+ }
+ // If this is a phantom record being updated from a concrete record, copy the ID in.
+ if (me.phantom && !sourceRecord.phantom) {
+ // beginEdit to prevent events firing
+ // commit at the end to prevent dirty being set
+ me.beginEdit();
+ me.setId(sourceRecord.getId());
+ me.endEdit(true);
+ me.commit(true);
+ }
+ }
+ return modifiedFieldNames;
+ },
+ /**
+ * Helper function used by afterEdit, afterReject and afterCommit. Calls the given
+ * method on the `Ext.data.Store` that this instance has {@link #join joined}, if any.
+ * The store function will always be called with the model instance as its single
+ * argument. If this model is joined to a Ext.data.NodeStore, then this method calls
+ * the given method on the NodeStore and the associated Ext.data.TreeStore.
+ * @param {String} funcName The name function to call on each store.
+ * @param {Array} [args] The arguments to pass to the method. This instance is
+ * always inserted as the first argument.
+ * @private
+ */
+ callJoined: function(funcName, args) {
+ var me = this,
+ joined = me.joined,
+ session = me.session,
+ state = me.dropped ? 'D' : (me.phantom ? 'C' : (me.dirty ? 'U' : 'R')),
+ i, len, fn, item;
+ me.crudState = state;
+ if (joined || session) {
+ if (args) {
+ args.unshift(me);
+ } else {
+ args = [
+ me
+ ];
+ }
+ fn = session && session[funcName];
+ if (fn) {
+ fn.apply(session, args);
+ }
+ if (joined) {
+ for (i = 0 , len = joined.length; i < len; ++i) {
+ item = joined[i];
+ if (item && (fn = item[funcName])) {
+ fn.apply(item, args);
+ }
+ }
+ }
+ }
+ me.crudStateWas = state;
+ },
+ /**
+ * Currently this only checks the loading state, this method exists for API
+ * parity with stores.
+ * @return {Boolean} `true` if the model is loading or has a pending load.
+ *
+ * @private
+ */
+ hasPendingLoad: function() {
+ return this.isLoading();
+ },
+ interpret: function(name) {
+ var me = this,
+ accessor = me._metaProperties[name];
+ if (!accessor) {
+ accessor = me.associations;
+ // e.g. "orderItems"
+ accessor = accessor && accessor[name] && accessor[name].getterName;
+ }
+ if (accessor) {
+ return me[accessor]();
+ }
+ // e.g., me.isPhantom()
+ return me.data[name];
+ },
+ /**
+ * Gets the dirty state of this record.
+ * @return {Boolean} The dirty state.
+ *
+ * @private
+ */
+ isDirty: function() {
+ // Added as a method to be used by data binding
+ return this.dirty;
+ },
+ /**
+ * Gets the phantom state of this record.
+ * @return {Boolean} The phantom state.
+ *
+ * @private
+ */
+ isPhantom: function() {
+ // Added as a method to be used by data binding
+ return this.phantom;
+ },
+ /**
+ * Called when an associated record instance has been set.
+ * @param {Ext.data.Model} record The record.
+ * @param {Ext.data.schema.Role} role The role.
+ *
+ * @private
+ */
+ onAssociatedRecordSet: function(record, role) {
+ this.callJoined('afterAssociatedRecordSet', [
+ record,
+ role
+ ]);
+ },
+ /**
+ * @method
+ * Called when the model id is changed.
+ * @param {Object} id The new id.
+ * @param {Object} oldId The old id.
+ */
+ onIdChanged: Ext.privateFn,
+ /**
+ * Set the session for this record.
+ * @param {Ext.data.Session} session The session
+ */
+ setSession: function(session) {
+ if (session) {
+ if (this.session) {
+ Ext.raise('This model already belongs to a session.');
+ }
+ if (!this.id) {
+ Ext.raise('The model must have an id to participate in a session.');
+ }
+ }
+ this.session = session;
+ if (session) {
+ session.add(this);
+ }
+ },
+ /**
+ * Gets the names of all the fields that were modified during an edit.
+ * @param {Object} [old] The saved data from `beginEdit`.
+ * @return {String[]} The array of modified field names.
+ * @private
+ */
+ getModifiedFieldNames: function(old) {
+ var me = this,
+ data = me.data,
+ modified = [],
+ oldData = old || me.editMemento.data,
+ key;
+ for (key in data) {
+ if (data.hasOwnProperty(key)) {
+ if (!me.isEqual(data[key], oldData[key], key)) {
+ modified.push(key);
+ }
+ }
+ }
+ return modified;
+ },
+ /**
+ * Checks if two values are equal, taking into account certain special factors, for
+ * example dates.
+ * @param {Object} lhs The first value.
+ * @param {Object} rhs The second value.
+ * @param {String/Ext.data.Field} [field] The field name or instance.
+ * @return {Boolean} True if the values are equal.
+ * @private
+ */
+ isEqual: function(lhs, rhs, field) {
+ var f;
+ if (field) {
+ f = field.isField ? field : this.fieldsMap[field];
+ if (f) {
+ return f.isEqual(lhs, rhs);
+ }
+ }
+ // instanceof is ~10 times faster then Ext.isDate. Values here will not be
+ // cross-document objects
+ if (lhs instanceof Date && rhs instanceof Date) {
+ return lhs.getTime() === rhs.getTime();
+ }
+ return lhs === rhs;
+ },
+ statics: {
+ /**
+ * @property
+ * @static
+ * @private
+ * @readonly
+ * @deprecated 5.0 Use the string `"edit"` directly.
+ * The update operation of type 'edit'. Used by the
+ * {@link Ext.data.Store#event-update Store.update} event.
+ */
+ EDIT: 'edit',
+ /**
+ * @property
+ * @static
+ * @private
+ * @readonly
+ * @deprecated 5.0 Use the string `"reject"` directly.
+ * The update operation of type 'reject'. Used by the
+ * {@link Ext.data.Store#event-update Store.update} event.
+ */
+ REJECT: 'reject',
+ /**
+ * @property
+ * @static
+ * @private
+ * @readonly
+ * @deprecated 5.0 Use the string `"commit"` directly.
+ * The update operation of type 'commit'. Used by the
+ * {@link Ext.data.Store#event-update Store.update} event.
+ */
+ COMMIT: 'commit',
+ rankFields: function() {
+ var cls = this,
+ prototype = cls.prototype,
+ fields = cls.fields,
+ length = fields.length,
+ rankedFields = [],
+ criticalFields = [],
+ transientFields = [],
+ evilFields, field, i;
+ cls.rankedFields = prototype.rankedFields = rankedFields;
+ cls.criticalFields = prototype.criticalFields = criticalFields;
+ cls.transientFields = prototype.transientFields = transientFields;
+ // This first pass brings over any fields that have no dependencies at all
+ // and gathers the evil fields to the side (the fields that could depend on
+ // anything). This avoids the call to topoAdd that we must perform on all of
+ // the fields that do have depends (which is good since most fields will be
+ // handled here).
+ for (i = 0; i < length; ++i) {
+ field = fields[i];
+ if (field.critical) {
+ criticalFields.push(field);
+ }
+ if (!field.persist) {
+ transientFields.push(field);
+ }
+ if (field.evil) {
+ (evilFields || (evilFields = [])).push(field);
+ } else if (!field.depends) {
+ rankedFields.push(field);
+ field.rank = rankedFields.length;
+ }
+ }
+ // 1-based
+ for (i = 0; i < length; ++i) {
+ if (!(field = fields[i]).rank && !field.evil) {
+ cls.topoAdd(field);
+ }
+ }
+ if (evilFields) {
+ for (i = 0 , length = evilFields.length; i < length; ++i) {
+ rankedFields.push(field = evilFields[i]);
+ field.rank = rankedFields.length;
+ }
+ }
+ // 1-based
+ cls.topoStack = null;
+ // cleanup diagnostic stack
+ return rankedFields;
+ },
+ topoAdd: function(field) {
+ var cls = this,
+ dep = field.depends,
+ dependsLength = dep ? dep.length : 0,
+ rankedFields = cls.rankedFields,
+ i, targetField;
+ /* eslint-disable-next-line vars-on-top, one-var */
+ var topoStack = cls.topoStack || (cls.topoStack = []);
+ topoStack.push(field.name);
+ if (field.rank === 0) {
+ // if (adding)
+ Ext.raise(cls.$className + " has circular field dependencies: " + topoStack.join(" --> "));
+ }
+ if (topoStack.length && field.evil) {
+ Ext.raise(cls.$className + ": Field " + topoStack[topoStack.length - 1] + " cannot depend on depends-less field " + field.name);
+ }
+ field.rank = 0;
+ // adding (falsey but we can still detect cycles)
+ for (i = 0; i < dependsLength; ++i) {
+ // Get the targetField on which we depend and add this field to the
+ // targetField.dependents[]
+ targetField = cls.fieldsMap[dep[i]];
+ if (!targetField) {
+ Ext.raise(cls.$className + ": Field " + field.name + " depends on undefined field " + dep[i]);
+ }
+ (targetField.dependents || (targetField.dependents = [])).push(field);
+ if (!targetField.rank) {
+ // if (!added)
+ cls.topoAdd(targetField);
+ }
+ }
+ rankedFields.push(field);
+ field.rank = rankedFields.length;
+ // 1-based (truthy to track "added" state)
+ topoStack.pop();
+ },
+ initFields: function(data, cls, proto) {
+ var Field = Ext.data.field.Field,
+ fieldDefs = data.fields,
+ // allocate fields [] and ordinals {} for the new class:
+ fields = [],
+ fieldOrdinals = {},
+ fieldsMap = {},
+ references = [],
+ superFields = proto.fields,
+ versionProperty = data.versionProperty || proto.versionProperty,
+ idProperty = cls.idProperty,
+ idField, field, i, length, name, ordinal, reference, superIdField, superIdFieldName, superIdDeclared, idDeclared;
+ // Process any inherited fields to produce a fields [] and ordinals {} for
+ // this class:
+ cls.fields = proto.fields = fields;
+ cls.fieldOrdinals = proto.fieldOrdinals = fieldOrdinals;
+ cls.fieldsMap = proto.fieldsMap = fieldsMap;
+ cls.references = proto.references = references;
+ if (superFields) {
+ // We chain the super field so we can write to it
+ for (i = 0 , length = superFields.length; i < length; ++i) {
+ fields[i] = field = Ext.Object.chain(superFields[i]);
+ field.dependents = null;
+ // we need to recalculate these
+ field.owner = cls;
+ fieldOrdinals[name = field.name] = i;
+ fieldsMap[name] = field;
+ // Clear the rank because it needs to be set on the first pass through
+ // the fields in the subclass, don't inherit it from the parent
+ field.rank = null;
+ if (field.generated) {
+ superIdField = field;
+ superIdFieldName = field.name;
+ }
+ }
+ }
+ // Merge in any fields from this class:
+ delete data.fields;
+ if (fieldDefs) {
+ for (i = 0 , length = fieldDefs.length; i < length; ++i) {
+ field = fieldDefs[i];
+ reference = field.reference;
+ // Create a copy of the reference since we'll modify
+ // the reference on the field. Needed for subclasses
+ if (reference && typeof reference !== 'string') {
+ // Can have child objects, so merge it deeply
+ reference = Ext.merge({}, reference);
+ }
+ field.$reference = reference;
+ field = Field.create(fieldDefs[i]);
+ name = field.name;
+ ordinal = fieldOrdinals[name];
+ if (ordinal === undefined) {
+ // If the field is new, add it to the end of the fields[]
+ fieldOrdinals[name] = ordinal = fields.length;
+ }
+ // else, overwrite the field at the established ordinal
+ fieldsMap[name] = field;
+ fields[ordinal] = field;
+ field.definedBy = field.owner = cls;
+ field.ordinal = ordinal;
+ if (name === idProperty) {
+ idDeclared = field;
+ }
+ if (name === superIdFieldName) {
+ superIdDeclared = true;
+ }
+ }
+ }
+ // Lookup the idProperty in the ordinals map and create a synthetic field if
+ // we don't have one.
+ idField = fieldsMap[idProperty];
+ if (!idField) {
+ if (superIdField && superIdField.generated) {
+ ordinal = superIdField.ordinal;
+ } else {
+ ordinal = fields.length;
+ }
+ delete fieldsMap[superIdFieldName];
+ delete fieldOrdinals[superIdFieldName];
+ idField = new Field(idProperty);
+ fields[ordinal] = idField;
+ fieldOrdinals[idProperty] = ordinal;
+ fieldsMap[idProperty] = idField;
+ idField.definedBy = cls;
+ idField.ordinal = ordinal;
+ idField.generated = true;
+ } else if (idDeclared && !superIdDeclared && superIdField && superIdField.generated) {
+ // If we're declaring the id as a field in our fields array and it's different
+ // to the super id field that has been generated, pull it out and fix up
+ // the ordinals. This likely won't happen often, to do it earlier we would need
+ // to know the contents of the fields which would mean iterating over them
+ // twice.
+ Ext.Array.remove(fields, superIdField);
+ delete fieldsMap[superIdFieldName];
+ delete fieldOrdinals[superIdFieldName];
+ fieldsMap[idProperty] = idDeclared;
+ for (i = 0 , length = fields.length; i < length; ++i) {
+ field = fields[i];
+ fields.ordinal = i;
+ fieldOrdinals[field.name] = i;
+ }
+ }
+ idField.allowNull = idField.critical = idField.identifier = true;
+ idField.defaultValue = null;
+ cls.idField = proto.idField = idField;
+ if (versionProperty) {
+ field = fieldsMap[versionProperty];
+ if (!field) {
+ ordinal = fields.length;
+ field = new Field({
+ name: versionProperty,
+ type: 'int'
+ });
+ fields[ordinal] = field;
+ fieldOrdinals[versionProperty] = ordinal;
+ fieldsMap[versionProperty] = field;
+ field.definedBy = cls;
+ field.ordinal = ordinal;
+ field.generated = true;
+ }
+ field.defaultValue = 1;
+ field.critical = true;
+ }
+ },
+ // NOTE: Be aware that the one fellow that manipulates these after this
+ // point is Ext.data.NodeInterface.
+ initSummaries: function(data, cls, proto) {
+ var summaryDefs = data.summary,
+ superSummaries = proto.summaryFields,
+ summaries, summaryMap, name, summary, len, i, index, field;
+ if (superSummaries) {
+ summaries = [];
+ summaryMap = {};
+ for (i = 0 , len = superSummaries.length; i < len; ++i) {
+ summary = superSummaries[i];
+ summaries.push(summary);
+ summaries[summary.name] = i;
+ }
+ }
+ if (summaryDefs) {
+ delete data.summary;
+ summaries = summaries || [];
+ summaryMap = summaryMap || {};
+ for (name in summaryDefs) {
+ summary = summaryDefs[name];
+ if (typeof summary === 'function') {
+ summary = {
+ summary: summary
+ };
+ }
+ // If it's not in the summaries, it's new here. We've already
+ // applied when copying down so this is safe to do
+ index = summaryMap[name];
+ summary = Ext.apply({
+ name: name
+ }, summary);
+ field = summary.field;
+ if (field) {
+ delete summary.field;
+ summary.summaryField = field;
+ }
+ if (index === undefined) {
+ index = summaries.length;
+ summaryMap[name] = summary;
+ }
+ summaries[index] = summary;
+ }
+ }
+ if (summaries) {
+ for (i = 0 , len = summaries.length; i < len; ++i) {
+ if (summaries[i].name in proto.fieldsMap) {
+ Ext.raise('Cannot redefine field, use the summary property ' + 'on the field.');
+ }
+ }
+ // Store these in an array so we have a predictable order when subclassing
+ proto.summaryFields = summaries;
+ }
+ },
+ initValidators: function(data, cls, proto) {
+ var superValidators = proto.validators,
+ validators, field, copy, validatorDefs, i, length, fieldValidator, name, validator, item;
+ if (superValidators) {
+ validators = {};
+ for (field in superValidators) {
+ validators[field] = Ext.Array.clone(superValidators[field]);
+ }
+ }
+ validatorDefs = data.validators || data.validations;
+ if (data.validations) {
+ delete data.validations;
+ Ext.log.warn((cls.$className || 'Ext.data.Model') + ': validations has been deprecated. Please use validators ' + 'instead.');
+ }
+ if (validatorDefs) {
+ delete data.validators;
+ validators = validators || {};
+ // Support older array syntax
+ if (Ext.isArray(validatorDefs)) {
+ copy = {};
+ for (i = 0 , length = validatorDefs.length; i < length; ++i) {
+ item = validatorDefs[i];
+ name = item.field;
+ if (!copy[name]) {
+ copy[name] = [];
+ }
+ // Check for function form
+ item = item.fn || item;
+ copy[name].push(item);
+ }
+ validatorDefs = copy;
+ }
+ for (name in validatorDefs) {
+ fieldValidator = validatorDefs[name];
+ if (!Ext.isArray(fieldValidator)) {
+ fieldValidator = [
+ fieldValidator
+ ];
+ }
+ validator = validators[name];
+ if (validator) {
+ // Declared in super
+ Ext.Array.push(validator, fieldValidator);
+ } else {
+ validators[name] = fieldValidator;
+ }
+ }
+ }
+ if (validators) {
+ for (name in validators) {
+ field = cls.getField(name);
+ if (field) {
+ field.setModelValidators(validators[name]);
+ }
+ }
+ }
+ cls.validators = proto.validators = validators;
+ },
+ initAssociations: function(schema, data, cls) {
+ // Handle keyless associations
+ var associations = data.associations,
+ belongsTo = data.belongsTo,
+ hasMany = data.hasMany,
+ hasOne = data.hasOne,
+ // manyToMany can't be declared via reference
+ matrices = data.manyToMany,
+ i, length, assoc, o;
+ delete data.associations;
+ delete data.belongsTo;
+ delete data.hasMany;
+ delete data.hasOne;
+ delete data.manyToMany;
+ if (matrices) {
+ schema.addMatrices(cls, matrices);
+ }
+ if (associations) {
+ associations = Ext.isArray(associations) ? associations : [
+ associations
+ ];
+ for (i = 0 , length = associations.length; i < length; ++i) {
+ assoc = associations[i];
+ o = Ext.apply({}, assoc);
+ delete o.type;
+ switch (assoc.type) {
+ case 'belongsTo':
+ schema.addBelongsTo(cls, o);
+ break;
+ case 'hasMany':
+ schema.addHasMany(cls, o);
+ break;
+ case 'hasOne':
+ schema.addHasOne(cls, o);
+ break;
+ default:
+ Ext.raise('Invalid association type: "' + assoc.type + '"');
+ }
+ }
+ }
+ if (belongsTo) {
+ belongsTo = Ext.isArray(belongsTo) ? belongsTo : [
+ belongsTo
+ ];
+ for (i = 0 , length = belongsTo.length; i < length; ++i) {
+ schema.addBelongsTo(cls, belongsTo[i]);
+ }
+ }
+ if (hasMany) {
+ hasMany = Ext.isArray(hasMany) ? hasMany : [
+ hasMany
+ ];
+ for (i = 0 , length = hasMany.length; i < length; ++i) {
+ schema.addHasMany(cls, hasMany[i]);
+ }
+ }
+ if (hasOne) {
+ hasOne = Ext.isArray(hasOne) ? hasOne : [
+ hasOne
+ ];
+ for (i = 0 , length = hasOne.length; i < length; ++i) {
+ schema.addHasOne(cls, hasOne[i]);
+ }
+ }
+ schema.afterKeylessAssociations(cls);
+ },
+ initIdentifier: function(data, cls, proto) {
+ var identifier = data.identifier || data.idgen,
+ superIdent = proto.identifier || cls.schema._defaultIdentifier,
+ generatorPrefix;
+ if (data.idgen) {
+ Ext.log.warn('Ext.data.Model: idgen has been deprecated. Please use ' + 'identifier instead.');
+ }
+ if (identifier) {
+ delete data.identifier;
+ delete data.idgen;
+ // An idgen was specified on the definition, use it explicitly.
+ identifier = Ext.Factory.dataIdentifier(identifier);
+ } else if (superIdent) {
+ // If we have a cloneable instance, and we don't have an id
+ // clone it. If we have an id, then we should use the same
+ // instance since it's the same as looking it up via id.
+ if (superIdent.clone && !superIdent.getId()) {
+ identifier = superIdent.clone();
+ } else if (superIdent.isGenerator) {
+ identifier = superIdent;
+ } else {
+ identifier = Ext.Factory.dataIdentifier(superIdent);
+ }
+ }
+ cls.identifier = proto.identifier = identifier;
+ if (!identifier) {
+ // If we didn't find one, create it and push it onto the class.
+ // Don't put it on the prototype, so a subclass will create
+ // it's own generator. If we have an anonymous model, go ahead and
+ // generate a unique prefix for it.
+ generatorPrefix = cls.entityName;
+ if (!generatorPrefix) {
+ generatorPrefix = Ext.id(null, 'extModel');
+ }
+ cls.identifier = Ext.Factory.dataIdentifier({
+ type: 'sequential',
+ prefix: generatorPrefix + '-'
+ });
+ }
+ },
+ findValidator: function(validators, name, cfg) {
+ var type = cfg.type || cfg,
+ field = validators[name],
+ len, i, item;
+ if (field) {
+ for (i = 0 , len = field.length; i < len; ++i) {
+ item = field[i];
+ if (item.type === type) {
+ return item;
+ }
+ }
+ }
+ return null;
+ },
+ /**
+ * This method produces the `initializeFn` for this class. If there are no fields
+ * requiring {@link Ext.data.field.Field#cfg-convert conversion} and no fields requiring
+ * a {@link Ext.data.field.Field#defaultValue default value} then this method will
+ * return `null`.
+ * @return {Function} The `initializeFn` for this class (or null).
+ * @private
+ */
+ makeInitializeFn: function(cls) {
+ var code = [
+ 'var '
+ ],
+ body = [
+ '\nreturn function (e) {\n var data = e.data, v;\n'
+ ],
+ work = 0,
+ bc, ec, // == beginClone, endClone
+ convert, expr, factory, field, fields, fs, hasDefValue, i, length;
+ if (!(fields = cls.rankedFields)) {
+ // On the first edit of a record of this type we need to ensure we have the
+ // topo-sort done:
+ fields = cls.rankFields();
+ }
+ for (i = 0 , length = fields.length; i < length; ++i) {
+ // The generated method declares vars for each field using "f0".."fN' as the
+ // name. These are used to access properties of the field (e.g., the convert
+ // method or defaultValue).
+ field = fields[i];
+ fs = 'f' + i;
+ convert = field.convert;
+ if (i) {
+ code.push(', \n ');
+ }
+ code.push(fs, ' = $fields[' + i + ']');
+ // this can be helpful when debugging (at least in Chrome):
+ code.push(' /* ', field.name, ' */');
+ // NOTE: added string literals are "folded" by the compiler so we
+ // are better off doing an "'foo' + 'bar'" then "'foo', 'bar'". But
+ // for variables we are better off pushing them into the array for
+ // the final join.
+ if ((hasDefValue = (field.defaultValue !== undefined)) || convert) {
+ // For non-calculated fields that have some work required (a convert method
+ // and/or defaultValue), generate a chunk of logic appropriate for the
+ // field.
+ // expr = data["fieldName"];
+ expr = 'data["' + field.name + '"]';
+ ++work;
+ bc = ec = '';
+ if (field.cloneDefaultValue) {
+ bc = 'Ext.clone(';
+ ec = ')';
+ }
+ body.push('\n');
+ if (convert && hasDefValue) {
+ // v = data.fieldName;
+ // if (v !== undefined) {
+ // v = f2.convert(v, e);
+ // }
+ // if (v === undefined) {
+ // v = f2.defaultValue;
+ // // or
+ // v = Ext.clone(f2.defaultValue);
+ // }
+ // data.fieldName = v;
+ //
+ body.push(' v = ', expr, ';\n' + ' if (v !== undefined) {\n' + ' v = ', fs, '.convert(v, e);\n' + ' }\n' + ' if (v === undefined) {\n' + ' v = ', bc, fs, '.defaultValue', ec, ';\n' + ' }\n' + ' ', expr, ' = v;');
+ } else if (convert) {
+ // no defaultValue
+ // v = f2.convert(data.fieldName,e);
+ // if (v !== undefined) {
+ // data.fieldName = v;
+ // }
+ //
+ body.push(' v = ', fs, '.convert(', expr, ',e);\n' + ' if (v !== undefined) {\n' + ' ', expr, ' = v;\n' + ' }\n');
+ } else if (hasDefValue) {
+ // no convert
+ // if (data.fieldName === undefined) {
+ // data.fieldName = f2.defaultValue;
+ // // or
+ // data.fieldName = Ext.clone(f2.defaultValue);
+ // }
+ //
+ body.push(' if (', expr, ' === undefined) {\n' + ' ', expr, ' = ', bc, fs, '.defaultValue', ec, ';\n' + ' }\n');
+ }
+ }
+ }
+ if (!work) {
+ // There are no fields that need special processing
+ return Ext.emptyFn;
+ }
+ code.push(';\n');
+ code.push.apply(code, body);
+ code.push('}');
+ code = code.join('');
+ // Ensure that Ext in the function code refers to the same Ext that we are
+ // using here. If we are in a sandbox, global.Ext might be different.
+ factory = new Function('$fields', 'Ext', code);
+ return factory(fields, Ext);
+ }
+ }
+ }
+}, // static
+// privates
+/* eslint-disable indent */
+function() {
+ var Model = this,
+ proto = Model.prototype,
+ Schema = Ext.data.schema.Schema,
+ defaultSchema;
+ Model.proxyConfig = proto.proxy;
+ delete proto.proxy;
+ // Base Model class may be used. It needs an empty fields array.
+ Model.fields = [];
+ // Base Model class may be used. It needs an empty fieldsMap hash.
+ Model.fieldsMap = proto.fieldsMap = {};
+ Model.schema = proto.schema = Schema.get(proto.schema);
+ proto.idField = new Ext.data.field.Field(proto.idProperty);
+ Model.identifier = new Ext.data.identifier.Sequential();
+ Model.onExtended(function(cls, data) {
+ var proto = cls.prototype,
+ schemaName = data.schema,
+ superCls = proto.superclass.self,
+ schema, entityName, proxy;
+ cls.idProperty = data.idProperty || proto.idProperty;
+ if (schemaName) {
+ delete data.schema;
+ schema = Schema.get(schemaName);
+ } else if (!(schema = proto.schema)) {
+ schema = defaultSchema || (defaultSchema = Schema.get('default'));
+ }
+ // These are in "privates" so we manually make them inherited:
+ cls.rankFields = Model.rankFields;
+ cls.topoAdd = Model.topoAdd;
+ // if we picked up a schema from cls.prototype.schema, it is because it was found
+ // in the prototype chain on a base class.
+ proto.schema = cls.schema = schema;
+ // Unless specified on the declaration data, we need to provide the entityName of
+ // the new Entity-derived class. Store it on the prototype and the class.
+ if (!(entityName = data.entityName)) {
+ proto.entityName = entityName = schema.getEntityName(cls);
+ if (!entityName) {
+ if (data.associations) {
+ Ext.raise('Anonymous entities cannot specify "associations"');
+ }
+ if (data.belongsTo) {
+ Ext.raise('Anonymous entities cannot specify "belongsTo"');
+ }
+ if (data.hasMany) {
+ Ext.raise('Anonymous entities cannot specify "hasMany"');
+ }
+ if (data.hasOne) {
+ Ext.raise('Anonymous entities cannot specify "hasOne"');
+ }
+ if (data.matrices) {
+ Ext.raise('Anonymous entities cannot specify "manyToMany"');
+ }
+ }
+ }
+ cls.entityName = entityName;
+ cls.fieldExtractors = {};
+ Model.initIdentifier(data, cls, proto);
+ Model.initFields(data, cls, proto);
+ Model.initValidators(data, cls, proto);
+ if (!data.isSummaryModel) {
+ Model.initSummaries(data, cls, proto);
+ }
+ // This is a compat hack to allow "rec.fields.items" to work as it used to when
+ // fields was a MixedCollection
+ cls.fields.items = cls.fields;
+ if (entityName) {
+ schema.addEntity(cls);
+ Model.initAssociations(schema, data, cls);
+ }
+ proxy = data.proxy;
+ if (proxy) {
+ delete data.proxy;
+ } else if (superCls !== Model) {
+ proxy = superCls.proxyConfig || superCls.proxy;
+ }
+ cls.proxyConfig = proxy;
+ });
+});
+
+/**
+ * @protected
+ * Simple wrapper class that represents a set of records returned by a Proxy.
+ */
+Ext.define('Ext.data.ResultSet', {
+ /**
+ * @property {Boolean} isResultSet
+ * Identifies this class as a result set.
+ */
+ isResultSet: true,
+ $configPrefixed: false,
+ config: {
+ /**
+ * @cfg {Boolean} loaded
+ * True if the records have already been loaded. This is only meaningful when dealing with
+ * SQL-backed proxies.
+ */
+ loaded: true,
+ /**
+ * @cfg {Number} count
+ * The number of records in this ResultSet. Note that total may differ from this number.
+ */
+ count: null,
+ /**
+ * @cfg {Number} total
+ * The total number of records reported by the data source. This ResultSet may form
+ * a subset of those records (see {@link #count}).
+ */
+ total: null,
+ /**
+ * @cfg {Number} remoteTotal
+ * The total number of records reported by the remote data source.
+ */
+ remoteTotal: null,
+ /**
+ * @cfg {Boolean} success
+ * True if the ResultSet loaded successfully, false if any errors were encountered.
+ */
+ success: false,
+ /**
+ * @cfg {Ext.data.Model[]/Object[]} records (required)
+ * The array of record instances or record config objects.
+ */
+ records: null,
+ /**
+ * @cfg {String} message
+ * The message that was read in from the data
+ */
+ message: null,
+ /**
+ * @cfg {Object} metadata
+ * The metadata object from a server sourced JSON data packet.
+ */
+ metadata: null,
+ /**
+ * @cfg {Ext.data.Model[]} groupData
+ * The grouping data.
+ */
+ groupData: null,
+ /**
+ * @cfg {Ext.data.Model} summaryData
+ * The summary data.
+ */
+ summaryData: null
+ },
+ /**
+ * Creates the resultSet
+ * @param {Object} [config] Config object.
+ */
+ constructor: function(config) {
+ this.initConfig(config);
+ },
+ getCount: function() {
+ var count = this.callParent(),
+ records;
+ if (!count) {
+ records = this.getRecords();
+ if (records) {
+ count = records.length;
+ }
+ }
+ return count;
+ }
+});
+
+/**
+ * Readers are used to interpret data to be loaded into a {@link Ext.data.Model Model} instance
+ * or a {@link Ext.data.Store Store} - often in response to an AJAX request. In general there is
+ * usually no need to create a Reader instance directly, since a Reader is almost always used
+ * together with a {@link Ext.data.proxy.Proxy Proxy}, and is configured using the Proxy's
+ * {@link Ext.data.proxy.Proxy#cfg-reader reader} configuration property:
+ *
+ * Ext.create('Ext.data.Store', {
+ * model: 'User',
+ * proxy: {
+ * type: 'ajax',
+ * url: 'users.json',
+ * reader: {
+ * type: 'json',
+ * rootProperty: 'users'
+ * }
+ * },
+ * });
+ *
+ * The above reader is configured to consume a JSON string that looks something like this:
+ *
+ * {
+ * "success": true,
+ * "users": [
+ * { "name": "User 1" },
+ * { "name": "User 2" }
+ * ]
+ * }
+ *
+ *
+ * # Loading Nested Data
+ *
+ * Readers have the ability to automatically load deeply-nested data objects based on the
+ * {@link Ext.data.schema.Association associations} configured on each Model. Below is an example
+ * demonstrating the flexibility of these associations in a fictional CRM system which manages
+ * a User, their Orders, OrderItems and Products. First we'll define the models:
+ *
+ * Ext.define("User", {
+ * extend: 'Ext.data.Model',
+ * fields: [
+ * 'id', 'name'
+ * ],
+ *
+ * hasMany: {model: 'Order', name: 'orders'},
+ *
+ * proxy: {
+ * type: 'rest',
+ * url : 'users.json',
+ * reader: {
+ * type: 'json',
+ * rootProperty: 'users'
+ * }
+ * }
+ * });
+ *
+ * Ext.define("Order", {
+ * extend: 'Ext.data.Model',
+ * fields: [
+ * 'id', 'total'
+ * ],
+ *
+ * hasMany : {model: 'OrderItem', name: 'orderItems', associationKey: 'order_items'},
+ * belongsTo: 'User'
+ * });
+ *
+ * Ext.define("OrderItem", {
+ * extend: 'Ext.data.Model',
+ * fields: [
+ * 'id', 'price', 'quantity', 'order_id', 'product_id'
+ * ],
+ *
+ * belongsTo: ['Order', {model: 'Product', associationKey: 'product'}]
+ * });
+ *
+ * Ext.define("Product", {
+ * extend: 'Ext.data.Model',
+ * fields: [
+ * 'id', 'name'
+ * ],
+ *
+ * hasMany: 'OrderItem'
+ * });
+ *
+ * This may be a lot to take in - basically a User has many Orders, each of which is composed of
+ * several OrderItems. Finally, each OrderItem has a single Product. This allows us to consume data
+ * like this:
+ *
+ * {
+ * "users": [
+ * {
+ * "id": 123,
+ * "name": "Ed",
+ * "orders": [
+ * {
+ * "id": 50,
+ * "total": 100,
+ * "order_items": [
+ * {
+ * "id": 20,
+ * "price": 40,
+ * "quantity": 2,
+ * "product": {
+ * "id": 1000,
+ * "name": "MacBook Pro"
+ * }
+ * },
+ * {
+ * "id": 21,
+ * "price": 20,
+ * "quantity": 3,
+ * "product": {
+ * "id": 1001,
+ * "name": "iPhone"
+ * }
+ * }
+ * ]
+ * }
+ * ]
+ * }
+ * ]
+ * }
+ *
+ * The JSON response is deeply nested - it returns all Users (in this case just 1 for simplicity's
+ * sake), all of the Orders for each User (again just 1 in this case), all of the OrderItems
+ * for each Order (2 order items in this case), and finally the Product associated with each
+ * OrderItem. Now we can read the data and use it as follows:
+ *
+ * var store = Ext.create('Ext.data.Store', {
+ * model: "User"
+ * });
+ *
+ * store.load({
+ * callback: function() {
+ * // the user that was loaded
+ * var user = store.first();
+ *
+ * console.log("Orders for " + user.get('name') + ":")
+ *
+ * // iterate over the Orders for each User
+ * user.orders().each(function(order) {
+ * console.log("Order ID: " + order.getId() + ", which contains items:");
+ *
+ * // iterate over the OrderItems for each Order
+ * order.orderItems().each(function(orderItem) {
+ * // we know that the Product data is already loaded,
+ * // so we can use the synchronous getProduct
+ * // usually, we would use the asynchronous version (see #belongsTo)
+ * var product = orderItem.getProduct();
+ *
+ * console.log(orderItem.get('quantity') + ' orders of ' + product.get('name'));
+ * });
+ * });
+ * }
+ * });
+ *
+ * Running the code above results in the following:
+ *
+ * Orders for Ed:
+ * Order ID: 50, which contains items:
+ * 2 orders of MacBook Pro
+ * 3 orders of iPhone
+ */
+Ext.define('Ext.data.reader.Reader', {
+ alternateClassName: [
+ 'Ext.data.Reader',
+ 'Ext.data.DataReader'
+ ],
+ mixins: [
+ Ext.mixin.Observable,
+ Ext.mixin.Factoryable
+ ],
+ alias: 'reader.base',
+ factoryConfig: {
+ defaultType: null
+ },
+ config: {
+ /**
+ * @cfg {String/Function} [groupRootProperty]
+ * Name of the property from which to retrieve remote grouping summary information.
+ * There should be an item for each group.
+ *
+ * The remote summary data should be parseable as a {@link #model} used by this reader.
+ */
+ groupRootProperty: '',
+ /**
+ * @cfg {Boolean} [implicitIncludes]
+ * True to automatically parse models nested within other models in a response object.
+ * See the Ext.data.reader.Reader intro docs for full explanation.
+ */
+ implicitIncludes: true,
+ /**
+ * @cfg {Boolean} [keepRawData] Determines if the Reader will keep raw data
+ * received from the server in the {@link #rawData} property.
+ *
+ * While this might seem useful to do additional data processing, keeping raw data
+ * might cause adverse effects such as memory leaks. It is recommended to set
+ * `keepRawData` to `false` if you do not need the raw data.
+ *
+ * If you need to process data packet to extract additional data such as row summaries,
+ * it is recommended to use {@link #transform} function for that purpose.
+ *
+ * Note that starting with Ext JS 6.0 the default behavior has been changed to
+ * **not** keep the raw data because of the high potential for memory leaks.
+ * @since 5.1.1
+ */
+ keepRawData: null,
+ /**
+ * @cfg {String/Function} messageProperty
+ * The name of the property which contains a response message for exception handling. If you
+ * want to return a false success response from the server, maybe due to some server-side
+ * validation, the messageProperty can hold the error message. For example:
+ *
+ * {
+ * "success": false,
+ * "error": "There was an error with your request"
+ * }
+ *
+ * You can retrieve this error message in a callback when loading a
+ * {@link Ext.data.Store Store} or {@link Ext.data.Model Model} like:
+ *
+ * var store = new Ext.data.Store({
+ * fields : ['foo'],
+ * proxy : {
+ * type : 'ajax',
+ * url : 'data.json',
+ * reader : {
+ * type : 'json',
+ * rootProperty : 'data',
+ * messageProperty : 'error'
+ * }
+ * }
+ * });
+ *
+ * store.load({
+ * callback: function(records, operation, success) {
+ * if (success) {
+ * // ...
+ * } else {
+ * var error = operation.getError();
+ *
+ * Ext.Msg.alert('Error', error);
+ * }
+ * }
+ * });
+ *
+ * In this example, the callback will execute with `success` being `false` and will
+ * therefore show the {@link Ext.MessageBox#alert Ext.Msg.alert} with the error string
+ * returned in the response.
+ */
+ messageProperty: '',
+ /**
+ * @cfg {String/Ext.data.Model} [model]
+ * The model to use for this reader. This config is only required if the reader is being
+ * used without a proxy, otherwise the proxy will automatically set the model.
+ */
+ model: null,
+ /**
+ * @cfg {Ext.data.proxy.Proxy} [proxy]
+ * The proxy attached to this reader. Typically only needed onMetaChange so that
+ * we can set the new model on the proxy.
+ * @private
+ */
+ proxy: null,
+ /**
+ * @cfg {Boolean} [readRecordsOnFailure]
+ * True to extract the records from a data packet even if the {@link #successProperty}
+ * returns false.
+ */
+ readRecordsOnFailure: true,
+ /**
+ * @cfg {String/Function} rootProperty
+ * The property that contains data items corresponding to the
+ * Model(s) of the configured Reader. `rootProperty` varies by Reader type.
+ *
+ * ##JSON Reader
+ * `rootProperty` is a property name. It may also be a dot-separated
+ * list of property names if the root is nested. The root JSON array will be
+ * used by default.
+ *
+ * // rootPropety config
+ * rootProperty: 'embedded.myresults'
+ *
+ * // server response
+ * {
+ * embedded: {
+ * myresults: [{
+ * name: 'Scott',
+ * age: 22
+ * }, {
+ * name: 'Ramona',
+ * age: 24
+ * }]
+ * },
+ * success: true
+ * }
+ *
+ * ##XML Reader
+ * `rootProperty` is a CSS selector. The root XML element will be used
+ * by default.
+ *
+ * // rootProperty config (plus record config)
+ * rootProperty: 'myresults',
+ * record: 'user'
+ *
+ * // server response
+ *
+ *
+ *
+ *
+ * Scott
+ * 22
+ *
+ *
+ * Ramona
+ * 24
+ *
+ *
+ *
+ *
+ * ##Array Reader
+ * `rootProperty` is not typically applicable since the data is assumed to be a
+ * single-level array of arrays. However, if the array of records is returned
+ * within a JSON response a `rootProperty` config may be used:
+ *
+ * // rootProperty config
+ * rootProperty: 'embedded.myresults'
+ *
+ * // server response
+ * {
+ * embedded: {
+ * myresults: [['Scott', 22], ['Ramona', 24]]
+ * },
+ * success: true
+ * }
+ *
+ * ##rootProperty as a function
+ * The `rootProperty` may also be a function that returns the root node from
+ * the dataset. For example:
+ *
+ * var store = Ext.create('Ext.data.TreeStore', {
+ * proxy: {
+ * type: 'memory',
+ * reader: {
+ * type: 'json',
+ * rootProperty: function(data){
+ * // Extract child nodes from the items or children property
+ * // in the dataset
+ * return data.items || data.children;
+ * }
+ * }
+ * },
+ * data: {
+ * items: [{
+ * text: 'item 1',
+ * children: [{
+ * text: 'child A',
+ * leaf: true
+ * }]
+ * }]
+ * }
+ * });
+ *
+ * Ext.create('Ext.tree.Panel', {
+ * title: 'rootProperty as a function',
+ * width: 200,
+ * height:150,
+ * store: store,
+ * rootVisible: false,
+ * renderTo: Ext.getBody()
+ * });
+ */
+ rootProperty: '',
+ /**
+ * @cfg {String} [successProperty]
+ * Name of the property from which to retrieve the `success` attribute, the value of which
+ * indicates whether a given request succeeded or failed (typically a boolean or
+ * 'true'|'false'). See
+ * {@link Ext.data.proxy.Server}.{@link Ext.data.proxy.Server#exception exception} for
+ * additional information.
+ */
+ successProperty: 'success',
+ /**
+ * @cfg {String/Function} [summaryRootProperty]
+ * Name of the property from which to retrieve remote summary information.
+ *
+ * The remote summary data should be parseable as a {@link #model} used by this reader.
+ */
+ summaryRootProperty: '',
+ /**
+ * @cfg {String} [totalProperty]
+ * Name of the property from which to retrieve the total number of records in the dataset.
+ * This is only needed if the whole dataset is not passed in one go, but is being paged from
+ * the remote server.
+ */
+ totalProperty: 'total',
+ /**
+ * @cfg {Function|String|Object} [transform]
+ * If a transform function is set, it will be invoked just before {@link #readRecords}
+ * executes. It is passed the raw (deserialized) data object. The transform function
+ * returns a data object, which can be a modified version of the original data object,
+ * or a completely new data object. The transform can be a function, or a method name
+ * on the Reader instance, or an object with a 'fn' key and an optional 'scope' key.
+ *
+ * Example usage:
+ *
+ * Ext.create('Ext.data.Store', {
+ * model: 'User',
+ * proxy: {
+ * type: 'ajax',
+ * url : 'users.json',
+ * reader: {
+ * type: 'json',
+ * transform: {
+ * fn: function(data) {
+ * // do some manipulation of the raw data object
+ * return data;
+ * },
+ * scope: this
+ * }
+ * }
+ * },
+ * });
+ *
+ */
+ transform: null,
+ /**
+ * @cfg {String} [typeProperty]
+ * The name of the property in a node raw data block which indicates the type of the model
+ * to be created from that raw data. Useful for heterogeneous trees.
+ *
+ * For example, hierarchical geographical data may look like this:
+ *
+ * {
+ * nodeType: 'Territory',
+ * name: 'EMEA',
+ * children: [{
+ * nodeType: 'Country',
+ * name: 'United Kingdon',
+ * children: [{
+ * nodeType: 'City',
+ * name: 'London'
+ * }]
+ * }]
+ * }
+ *
+ * You would configure the typeProperty in this case to be `"nodeType"` which would cause
+ * the models named "Territory", "Country" and "City" to be used.
+ */
+ typeProperty: ''
+ },
+ /**
+ * @property {Object} rawData
+ * The raw data object that was last passed to {@link #readRecords}. rawData is populated
+ * based on the results of {@link Ext.data.proxy.Server#processResponse}. rawData will
+ * maintain a cached copy of the last successfully returned records. In other words,
+ * if processResponse is unsuccessful, the records from the last successful response
+ * will remain cached in rawData.
+ *
+ * Since Ext JS 5.1.1 you can use the {@link #keepRawData} config option to
+ * control this behavior.
+ */
+ /**
+ * @property {Object} metaData
+ * The raw meta data that was most recently read, if any. Meta data can include existing
+ * Reader config options like {@link #totalProperty}, etc. that get
+ * automatically applied to the Reader, and those can still be accessed directly from the Reader
+ * if needed. However, meta data is also often used to pass other custom data to be processed
+ * by application code. For example, it is common when reconfiguring the data model of a grid to
+ * also pass a corresponding column model config to be applied to the grid. Any such data will
+ * not get applied to the Reader directly (it just gets passed through and is ignored by Ext).
+ * This metaData property gives you access to all meta data that was passed, including any such
+ * custom data ignored by the reader.
+ *
+ * This is a read-only property, and it will get replaced each time a new meta data object is
+ * passed to the reader. Note that typically you would handle proxy's
+ * {@link Ext.data.proxy.Proxy#metachange metachange} event which passes this exact same meta
+ * object to listeners. However this property is available if it's more convenient to access it
+ * via the reader directly in certain cases.
+ * @readonly
+ */
+ /**
+ * @property {Boolean} isReader
+ * `true` in this class to identify an object as an instantiated Reader, or subclass thereof.
+ */
+ isReader: true,
+ /**
+ * @event exception
+ * Fires when the reader receives improperly encoded data from the server
+ * @param {Ext.data.reader.Reader} reader A reference to this reader
+ * @param {XMLHttpRequest} response The XMLHttpRequest response object
+ * @param {Ext.data.ResultSet} error The error object
+ */
+ /**
+ * Creates new Reader.
+ * @param {Object} [config] Config object.
+ */
+ constructor: function(config) {
+ var me = this;
+ if (config && config.hasOwnProperty('root')) {
+ config = Ext.apply({}, config);
+ config.rootProperty = config.root;
+ delete config.root;
+ Ext.log.error('Ext.data.reader.Reader: Using the deprecated "root" configuration. ' + 'Use "rootProperty" instead.');
+ }
+ me.duringInit = 1;
+ // Will call initConfig
+ me.mixins.observable.constructor.call(me, config);
+ --me.duringInit;
+ me.buildExtractors();
+ },
+ forceBuildExtractors: function() {
+ if (!this.duringInit) {
+ this.buildExtractors(true);
+ }
+ },
+ updateGroupRootProperty: function() {
+ this.forceBuildExtractors();
+ },
+ updateMessageProperty: function() {
+ this.forceBuildExtractors();
+ },
+ applyModel: function(model) {
+ return Ext.data.schema.Schema.lookupEntity(model);
+ },
+ updateSuccessProperty: function() {
+ this.forceBuildExtractors();
+ },
+ updateTotalProperty: function() {
+ this.forceBuildExtractors();
+ },
+ applyTransform: function(transform) {
+ if (transform) {
+ if (Ext.isFunction(transform)) {
+ transform = {
+ fn: transform
+ };
+ } else if (transform.charAt) {
+ // faster than Ext.isString()
+ transform = {
+ fn: this[transform]
+ };
+ }
+ return transform.fn.bind(transform.scope || this);
+ }
+ return transform;
+ },
+ /**
+ * Reads the given response object. This method normalizes the different types of response
+ * object that may be passed to it. If it's an XMLHttpRequest object, hand off to the subclass'
+ * {@link #getResponseData} method. Else, hand off the reading of records to the
+ * {@link #readRecords} method.
+ * @param {Object} response The response object. This may be either an XMLHttpRequest object or
+ * a plain JS object
+ * @param {Object} [readOptions] Various options that instruct the reader on how to read the
+ * data
+ * @param {Function} [readOptions.recordCreator] A function to construct the model based on the
+ * processed data. By default, this just calls the model constructor and passes the raw data.
+ * @return {Ext.data.ResultSet} The parsed or default ResultSet object
+ */
+ read: function(response, readOptions) {
+ var data, result, responseText;
+ if (response) {
+ responseText = response.responseText;
+ if (response.responseType || responseText) {
+ result = this.getResponseData(response);
+ if (result && result.__$isError) {
+ return new Ext.data.ResultSet({
+ total: 0,
+ count: 0,
+ records: [],
+ success: false,
+ message: result.msg
+ });
+ } else {
+ data = this.readRecords(result, readOptions);
+ }
+ } else if (responseText !== '') {
+ data = this.readRecords(response, readOptions);
+ }
+ }
+ return data || this.nullResultSet;
+ },
+ /**
+ * Returns the shared null result set.
+ * @return {Ext.data.ResultSet} The null result set.
+ *
+ * @private
+ */
+ getNullResultSet: function() {
+ return this.nullResultSet;
+ },
+ /**
+ * Creates an object that identifies a read error occurred.
+ * @param {String} msg An error message to include
+ * @return {Object} An error object
+ *
+ * @private
+ */
+ createReadError: function(msg) {
+ return {
+ __$isError: true,
+ msg: msg
+ };
+ },
+ /**
+ * Abstracts common functionality used by all Reader subclasses. Each subclass is expected to
+ * call this function before running its own logic and returning the Ext.data.ResultSet
+ * instance. For most Readers additional processing should not be needed.
+ * @param {Object} data The raw data object
+ * @param {Object} [readOptions] See {@link #read} for details.
+ * @param {Object} [internalReadOptions] (private)
+ * @return {Ext.data.ResultSet} A ResultSet object
+ */
+ readRecords: function(data, readOptions, internalReadOptions) {
+ var me = this,
+ recordsOnly = internalReadOptions && internalReadOptions.recordsOnly,
+ asRoot = internalReadOptions && internalReadOptions.asRoot,
+ groupData = null,
+ summaryData = null,
+ success, recordCount, records, root, remoteTotal, total, value, message, transform, meta, summaryOptions;
+ // Extract the metadata to return with the ResultSet.
+ // If found reconfigure accordingly.
+ // The calling Proxy fires its metachange event if it finds metadata in the ResultSet.
+ meta = me.getMeta ? me.getMeta(data) : data.metaData;
+ if (meta) {
+ me.onMetaChange(meta);
+ }
+ transform = me.getTransform();
+ if (transform) {
+ data = transform(data);
+ }
+ me.buildExtractors();
+ if (me.getKeepRawData()) {
+ me.rawData = data;
+ }
+ if (me.hasListeners.rawdata) {
+ me.fireEventArgs('rawdata', [
+ data
+ ]);
+ }
+ data = me.getData(data);
+ success = true;
+ recordCount = 0;
+ records = [];
+ if (me.getSuccessProperty()) {
+ value = me.getSuccess(data);
+ if (value === false || value === 'false') {
+ success = false;
+ }
+ }
+ if (me.getMessageProperty()) {
+ message = me.getMessage(data);
+ }
+ // Only try and extract other data if call was successful
+ if (success || me.getReadRecordsOnFailure()) {
+ // If we pass an array as the data, we don't use getRoot on the data.
+ // Instead the root equals to the data.
+ root = (asRoot || Ext.isArray(data)) ? data : me.getRoot(data);
+ if (root) {
+ total = root.length;
+ }
+ if (me.getTotalProperty()) {
+ value = parseInt(me.getTotal(data), 10);
+ if (!isNaN(value)) {
+ remoteTotal = total = value;
+ }
+ }
+ if (root) {
+ records = me.extractData(root, readOptions);
+ recordCount = records.length;
+ }
+ if (me.getGroupRootProperty()) {
+ root = me.getGroupRoot(data);
+ if (root) {
+ summaryOptions = {
+ includes: false,
+ model: me.getModel().getSummaryModel()
+ };
+ groupData = me.extractData(root, summaryOptions) || null;
+ }
+ }
+ if (me.getSummaryRootProperty()) {
+ root = me.getSummaryRoot(data);
+ if (root) {
+ summaryOptions = summaryOptions || {
+ includes: false,
+ model: me.getModel().getSummaryModel()
+ };
+ summaryData = me.extractData(root, summaryOptions) || null;
+ // This always returns an array, so transform it
+ if (summaryData) {
+ summaryData = summaryData[0];
+ }
+ }
+ }
+ }
+ /* eslint-disable-next-line multiline-ternary */
+ return recordsOnly ? records : new Ext.data.ResultSet({
+ total: total || recordCount,
+ remoteTotal: remoteTotal,
+ metadata: meta,
+ count: recordCount,
+ records: records,
+ success: success,
+ message: message,
+ groupData: groupData,
+ summaryData: summaryData
+ });
+ },
+ /**
+ * Returns extracted, type-cast rows of data.
+ * @param {Object[]/Object} root from server response
+ * @param {Object} [readOptions] An object containing extra options.
+ * @param {Function} [readOptions.model] The Model constructor to use.
+ * @param {Function} [readOptions.recordCreator] A function to use to create and initialize
+ * records. By default a function is supplied which creates *non-phantom* records on the
+ * assumnption that a Reader is going to be used to read server-supplied data.
+ * @param {Object} [readOptions.recordCreator.data] The raw data used to create a record.
+ * @param {Function} [readOptions.recordCreator.Model] The Model constructor to use to create
+ * the record.
+ * @return {Array} An array of records containing the extracted data
+ * @private
+ */
+ extractData: function(root, readOptions) {
+ var me = this,
+ entityType = readOptions && readOptions.model ? Ext.data.schema.Schema.lookupEntity(readOptions.model) : me.getModel(),
+ // eslint-disable-line max-len
+ schema = entityType.schema,
+ includes = readOptions && 'includes' in readOptions ? readOptions.includes : schema.hasAssociations(entityType) && me.getImplicitIncludes(),
+ // eslint-disable-line max-len
+ fieldExtractorInfo = me.getFieldExtractorInfo(entityType),
+ length = root.length,
+ records = new Array(length),
+ typeProperty = me.getTypeProperty(),
+ reader, node, nodeType, record, i;
+ if (!length && Ext.isObject(root)) {
+ root = [
+ root
+ ];
+ length = 1;
+ }
+ for (i = 0; i < length; i++) {
+ record = root[i];
+ if (!record.isModel) {
+ // If we're given a model instance in the data, just push it on
+ // without doing any conversion. Otherwise, create a record.
+ node = record;
+ // This Reader may be configured to produce different model types based on
+ // a differentiator field in the incoming data:
+ // typeProperty name be a string, a function which yields the child type, or an
+ // object: {
+ // name: 'mtype',
+ // namespace: 'MyApp'
+ // }
+ if (typeProperty && (nodeType = me.getChildType(schema, node, typeProperty))) {
+ reader = nodeType.getProxy().getReader();
+ record = reader.extractRecord(node, readOptions, nodeType, schema.hasAssociations(nodeType) && reader.getImplicitIncludes(), reader.getFieldExtractorInfo(nodeType));
+ } else {
+ record = me.extractRecord(node, readOptions, entityType, includes, fieldExtractorInfo);
+ }
+ // Generally we don't want to have references to XML documents
+ // or XML nodes to hang around in memory but Trees need to be able
+ // to access the raw XML node data in order to process its children.
+ // See https://sencha.jira.com/browse/EXTJS-15785 and
+ // https://sencha.jira.com/browse/EXTJS-14286
+ if (record.isModel && record.isNode) {
+ record.raw = node;
+ }
+ }
+ if (record.onLoad) {
+ record.onLoad();
+ }
+ records[i] = record;
+ }
+ return records;
+ },
+ // Based upon a Reader's typeProperty config, determine the type of child node to create from
+ // the raw data
+ getChildType: function(schema, rawNode, typeProperty) {
+ var namespace;
+ switch (typeof typeProperty) {
+ case 'string':
+ return schema.getEntity(rawNode[typeProperty]);
+ case 'object':
+ namespace = typeProperty.namespace;
+ return schema.getEntity((namespace ? namespace + '.' : '') + rawNode[typeProperty.name]);
+ case 'function':
+ return schema.getEntity(typeProperty(rawNode));
+ }
+ },
+ extractRecordData: function(node, readOptions) {
+ /* eslint-disable-next-line max-len */
+ var entityType = readOptions && readOptions.model ? Ext.data.schema.Schema.lookupEntity(readOptions.model) : this.getModel(),
+ fieldExtractorInfo = this.getFieldExtractorInfo(entityType);
+ return this.extractRecord(node, readOptions, entityType, false, fieldExtractorInfo);
+ },
+ extractRecord: function(node, readOptions, entityType, includes, fieldExtractorInfo) {
+ var me = this,
+ creatorFn = (readOptions && readOptions.recordCreator) || me.defaultRecordCreator,
+ modelData, record;
+ // Create a record with an empty data object.
+ // Populate that data object by extracting and converting field values from raw data.
+ // Must pass the ID to use because we pass no data for the constructor to pluck an ID from
+ modelData = me.extractModelData(node, fieldExtractorInfo);
+ record = creatorFn.call(me, modelData, entityType || me.getModel(), readOptions);
+ if (includes && record.isModel) {
+ me.readAssociated(record, node, readOptions);
+ }
+ return record;
+ },
+ getFieldExtractorInfo: function(entityType) {
+ var extractors = entityType.fieldExtractors,
+ type, extractor;
+ // If the base Ext.data.Model class is being used, there will be no extractor info
+ // The raw data block will be imported unchanged.
+ if (!extractors) {
+ return;
+ }
+ type = this.$className;
+ extractor = extractors[type];
+ // If we have no extractors, buildFieldExtractors will return null,
+ // so we never need to rebuild them
+ if (extractor === undefined) {
+ extractors[type] = extractor = this.buildFieldExtractors(entityType);
+ }
+ return extractor;
+ },
+ buildFieldExtractors: function(entityType) {
+ var fields = entityType.getFields(),
+ len = fields.length,
+ buffer = [],
+ extractors = [],
+ out = null,
+ cnt = 0,
+ field, name, i, extractor;
+ for (i = 0; i < len; ++i) {
+ field = fields[i];
+ extractor = this.createFieldAccessor(field);
+ if (extractor) {
+ name = field.name;
+ // Use [] property access since we may have non-JS looking field names
+ buffer.push('val = extractors[' + cnt + '](raw, self); if (val !== undefined) { data[\'' + name + '\'] = val; }');
+ extractors.push(extractor);
+ ++cnt;
+ }
+ }
+ if (buffer.length) {
+ out = {
+ extractors: extractors,
+ fn: new Function('raw', 'data', 'extractors', 'self', 'var val;' + buffer.join('\n'))
+ };
+ }
+ return out;
+ },
+ defaultRecordCreator: function(data, Model) {
+ return new Model(data);
+ },
+ defaultRecordCreatorFromServer: function(data, Model) {
+ var record = new Model(data);
+ // If the server did not include an id in the response data, the Model constructor will
+ // mark the record as phantom. We need to set phantom to false here because records created
+ // from a server response using a reader by definition are not phantom records.
+ record.phantom = false;
+ return record;
+ },
+ getModelData: function(raw) {
+ return {};
+ },
+ extractModelData: function(raw, fieldExtractorInfo) {
+ var data = this.getModelData(raw),
+ fn;
+ // We may not have any mappings to process
+ if (fieldExtractorInfo) {
+ fn = fieldExtractorInfo.fn;
+ fn(raw, data, fieldExtractorInfo.extractors, this);
+ }
+ return data;
+ },
+ /**
+ * Loads the record associations from the data object.
+ * @param {Ext.data.Model} record The record to load associations for.
+ * @param {Object} data The raw data object.
+ * @param {Object} readOptions See {@link #read}.
+ *
+ * @private
+ */
+ readAssociated: function(record, data, readOptions) {
+ var roles = record.associations,
+ key, role;
+ for (key in roles) {
+ if (roles.hasOwnProperty(key)) {
+ role = roles[key];
+ // The class for the other role may not have loaded yet
+ if (role.cls) {
+ role.read(record, data, this, readOptions);
+ }
+ }
+ }
+ },
+ /**
+ * @method
+ * This method provides a hook to do any data transformation before the reading process
+ * begins. By default this function just returns what is passed to it. It can be
+ * overridden in a subclass to return something else.
+ * See {@link Ext.data.reader.Xml XmlReader} for an example.
+ *
+ * @param {Object} data The data object
+ * @return {Object} The normalized data object
+ * @protected
+ * @template
+ */
+ getData: Ext.identityFn,
+ /**
+ * @method
+ * This will usually need to be implemented in a subclass. Given a generic data object
+ * (the type depends on the type of data we are reading), this function should return the
+ * object as configured by the Reader's 'root' meta data config. See XmlReader's getRoot
+ * implementation for an example. By default the same data object will simply be returned.
+ *
+ * @param {Object} data The data object
+ * @return {Object} The same data object
+ * @private
+ */
+ getRoot: Ext.identityFn,
+ /**
+ * Takes a raw response object (as passed to the {@link #read} method) and returns the useful
+ * data segment from it. This must be implemented by each subclass.
+ * @param {Object} response The response object
+ * @return {Object} The extracted data from the response. For example, a JSON object or an XML
+ * document.
+ */
+ getResponseData: function(response) {
+ Ext.raise("getResponseData must be implemented in the Ext.data.reader.Reader subclass");
+ },
+ /**
+ * @private
+ * Reconfigures the meta data tied to this Reader
+ */
+ onMetaChange: function(meta) {
+ var me = this,
+ fields = meta.fields,
+ model, newModel, clientIdProperty, proxy;
+ // save off the raw meta data
+ me.metaData = meta;
+ // set any reader-specific configs from meta if available
+ if (meta.root) {
+ me.setRootProperty(meta.root);
+ }
+ if (meta.totalProperty) {
+ me.setTotalProperty(meta.totalProperty);
+ }
+ if (meta.successProperty) {
+ me.setSuccessProperty(meta.successProperty);
+ }
+ if (meta.messageProperty) {
+ me.setMessageProperty(meta.messageProperty);
+ }
+ clientIdProperty = meta.clientIdProperty;
+ if (fields) {
+ newModel = Ext.define(null, {
+ extend: 'Ext.data.Model',
+ fields: fields,
+ clientIdProperty: clientIdProperty
+ });
+ me.setModel(newModel);
+ proxy = me.getProxy();
+ if (proxy) {
+ proxy.setModel(newModel);
+ }
+ } else if (clientIdProperty) {
+ model = me.getModel();
+ if (model) {
+ model.self.prototype.clientIdProperty = clientIdProperty;
+ }
+ }
+ },
+ /**
+ * @private
+ * This builds optimized functions for retrieving record data and meta data from an object.
+ * Subclasses may need to implement their own getRoot function.
+ * @param {Boolean} [force=false] True to automatically remove existing extractor functions
+ * first
+ */
+ buildExtractors: function(force) {
+ var me = this,
+ totalProp, successProp, messageProp;
+ if (force || !me.hasExtractors) {
+ totalProp = me.getTotalProperty();
+ successProp = me.getSuccessProperty();
+ messageProp = me.getMessageProperty();
+ // build the extractors for all the meta data
+ if (totalProp) {
+ me.getTotal = me.getAccessor(totalProp);
+ }
+ if (successProp) {
+ me.getSuccess = me.getAccessor(successProp);
+ }
+ if (messageProp) {
+ me.getMessage = me.getAccessor(messageProp);
+ }
+ me.hasExtractors = true;
+ return true;
+ }
+ },
+ getAccessor: function(prop) {
+ var me = this,
+ cache = me.extractorCache,
+ ret, key;
+ if (typeof prop === 'string') {
+ key = me.getAccessorKey(prop);
+ if (key) {
+ ret = cache.get(key);
+ }
+ if (!ret) {
+ ret = me.createAccessor(prop);
+ if (key) {
+ cache.add(key, ret);
+ }
+ }
+ } else {
+ ret = me.createAccessor(prop);
+ }
+ return ret;
+ },
+ getAccessorKey: function(prop) {
+ var className = this.$className;
+ return className ? className + prop : '';
+ },
+ createAccessor: Ext.emptyFn,
+ createFieldAccessor: Ext.emptyFn,
+ destroy: function() {
+ var me = this;
+ me.model = me.getTotal = me.getSuccess = me.getMessage = me.rawData = null;
+ // Proxy could have created a sequence
+ me.onMetaChange = null;
+ // Transform function can be bound
+ me.transform = null;
+ me.callParent();
+ },
+ privates: {
+ copyFrom: function(reader) {
+ var me = this;
+ reader.buildExtractors();
+ me.getTotal = reader.getTotal;
+ me.getSuccess = reader.getSuccess;
+ me.getMessage = reader.getMessage;
+ ++me.duringInit;
+ me.setConfig(reader.getConfig());
+ --me.duringInit;
+ me.hasExtractors = true;
+ },
+ getGroupRoot: Ext.privateFn,
+ getSummaryRoot: Ext.privateFn
+ }
+}, function(Cls) {
+ var proto = Cls.prototype;
+ Ext.apply(proto, {
+ // Private. Empty ResultSet to return when response is falsy (null|undefined|empty string)
+ nullResultSet: new Ext.data.ResultSet({
+ total: 0,
+ count: 0,
+ records: [],
+ success: true,
+ message: ''
+ })
+ });
+ proto.extractorCache = new Ext.util.LruCache();
+});
+
+/**
+ * Base Writer class used by most subclasses of {@link Ext.data.proxy.Server}. This class
+ * is responsible for taking a set of {@link Ext.data.operation.Operation} objects and a
+ * {@link Ext.data.Request} object and modifying that request based on the Operations.
+ *
+ * For example a Ext.data.writer.Json would format the Operations and their
+ * {@link Ext.data.Model} instances based on the config options passed to the JsonWriter's
+ * constructor.
+ *
+ * Writers are not needed for any kind of local storage - whether via a
+ * {@link Ext.data.proxy.WebStorage Web Storage proxy} (see
+ * {@link Ext.data.proxy.LocalStorage localStorage} and
+ * {@link Ext.data.proxy.SessionStorage sessionStorage})
+ * or just in memory via a {@link Ext.data.proxy.Memory MemoryProxy}.
+ *
+ * # Dates
+ *
+ * Before sending dates to the server, they can be formatted using an {@link Ext.Date}
+ * format. These formats can be specified both on the field and the writer itself. In terms
+ * of precedence, from highest to lowest:
+ *
+ * - {@link #dateFormat Writer.dateFormat} The writer `dateFormat` will always have the
+ * highest precedence.
+ * - {@link Ext.data.field.Date#dateWriteFormat} The `dateWriteFormat` given to the field
+ * instance. This is handled by {@link Ext.data.field.Date#method-serialize}.
+ * - {@link Ext.data.field.Date#dateFormat Field.dateFormat} This is handled by the field's
+ * `serialize` method.
+ * - {@link Ext.data.field.Date#dateReadFormat Field.dateReadFormat} Also handled by the
+ * field's `serialize` method.
+ */
+Ext.define('Ext.data.writer.Writer', {
+ alias: 'writer.base',
+ alternateClassName: [
+ 'Ext.data.DataWriter',
+ 'Ext.data.Writer'
+ ],
+ mixins: [
+ Ext.mixin.Factoryable
+ ],
+ factoryConfig: {
+ defaultType: null
+ },
+ /**
+ * @property {Boolean} isWriter
+ * `true` to identify an object as an instance of this class, or subclass thereof.
+ * @readonly
+ */
+ isWriter: true,
+ config: {
+ /**
+ * @cfg {String} clientIdProperty
+ * When specified this property causes the `{@link Ext.data.Model#idProperty}` of
+ * newly created records to be sent to the server as this name instead of the
+ * value of the `idProperty`.
+ *
+ * For example, by default, the following code:
+ *
+ * Ext.define('Person', {
+ * idProperty: 'id', // this is the default value (for clarity)
+ *
+ * fields: [ 'name' ]
+ * });
+ *
+ * var person = new Person({
+ * // no id provided, so one is generated
+ * name: 'Clark Kent'
+ * });
+ *
+ * Will send this to the server:
+ *
+ * {
+ * id: 'Person-1',
+ * name: 'Clark Kent'
+ * }
+ *
+ * This can be an issue if the server expects an integer for the "id" property.
+ * You can use `{@link Ext.data.Model#identifier}` to produce identifiers that
+ * the server would recognize or use this config to send the client's id in a
+ * different property.
+ *
+ * Ext.define('Person', {
+ * idProperty: 'id', // this is the default value (for clarity)
+ *
+ * proxy: {
+ * writer: {
+ * clientIdProperty: 'clientId'
+ * }
+ * },
+ *
+ * fields: [ 'name' ]
+ * });
+ *
+ * Given the above, the server is sent this data now:
+ *
+ * {
+ * clientId: 'Person-1',
+ * name: 'Clark Kent'
+ * }
+ *
+ * While this config provides the behavior of `{@link Ext.data.Model#clientIdProperty}`
+ * from previous releases, this property is not as useful as a suitable
+ * `{@link Ext.data.Model#identifier}` due to id's appearing in foreign-key fields
+ * and in `{@link Ext.data.Model#manyToMany}` associations.
+ *
+ * See `{@link Ext.data.Model#identifier}` for more on id generation.
+ */
+ clientIdProperty: null,
+ /**
+ * @cfg {Object} allDataOptions
+ * This object contains the options passed to `{@link Ext.data.Model#getData}` when
+ * writing `{@link Ext.data.Model#phantom}` records or when `writeAllFields` is set
+ * to `true`.
+ *
+ * *NOTE:* The `serialize` option cannot be used here.
+ */
+ allDataOptions: {
+ persist: true
+ },
+ /**
+ * @cfg {Object} partialDataOptions
+ * This object contains the options passed to `{@link Ext.data.Model#getData}` when
+ * writing non `{@link Ext.data.Model#phantom}` records or when `writeAllFields` is
+ * set to `false`.
+ *
+ * *NOTE:* The `serialize` option cannot be used here.
+ */
+ partialDataOptions: {
+ changes: true,
+ critical: true
+ },
+ /**
+ * @cfg {Boolean} writeAllFields `true` to write all fields from the record to the
+ * server. If set to `false` it will only send the fields that were modified. Note
+ * that any fields that have `{@link Ext.data.field.Field#persist}` set to `false`
+ * will still be ignored while those with `{@link Ext.data.field.Field#critical}`
+ * set to `true` will be included.
+ *
+ * The exact set of fields written is determined by `allDataOptions` (when `true`)
+ * or `partialDataOptions` (when `false`). This option is ignored and treated as
+ * `true` when writing `{@link Ext.data.Model#phantom}` records.
+ *
+ * It is seldom a good idea to use this config. Rather use `allDataOptions` or
+ * `partialDataOptions` to control what fields are sent for records based on their
+ * `{@link Ext.data.Model#phantom}` state.
+ *
+ * In the previous release, this was default `true`.
+ */
+ writeAllFields: false,
+ /**
+ * @cfg {String} dateFormat
+ * This is used for each field of type date in the model to format the value before
+ * it is sent to the server.
+ */
+ dateFormat: null,
+ /**
+ * @cfg {String} nameProperty
+ * This property is used to read the key for each value that will be sent to the
+ * server.
+ *
+ * For example:
+ *
+ * Ext.define('Person', {
+ * extend: 'Ext.data.Model',
+ * fields: [{
+ * name: 'first',
+ * mapping: 'firstName'
+ * }, {
+ * name: 'last',
+ * mapping: 'lastName'
+ * }, {
+ * name: 'age'
+ * }]
+ * });
+ *
+ * new Ext.data.writer.Writer({
+ * nameProperty: 'mapping'
+ * });
+ *
+ * // This will be sent to the server
+ * {
+ * firstName: 'first name value',
+ * lastName: 'last name value',
+ * age: 1
+ * }
+ *
+ * If the value is not present, the field name will always be used.
+ */
+ nameProperty: 'name',
+ /**
+ * @cfg {Boolean} [writeRecordId]
+ * By default, each record's id is always included in the output for non-phantom
+ * records since in most cases the id will be required on the server to process
+ * the record action. This is helpful since the id will normally not be modified,
+ * and so would not be sent to the server unless {@link #writeAllFields} was
+ * explicitly enabled.
+ *
+ * However, there are cases where it is not desirable for the record id to be passed
+ * in the data directly. For example, when using a RESTful API the record id would
+ * typically be appended to the url instead.
+ */
+ writeRecordId: true,
+ /**
+ * @cfg {Function|Object} [transform]
+ * If a transform function is set, it will be invoked just before {@link #writeRecords}
+ * executes. It is passed the unserialized data object and the
+ * {@link Ext.data.Request request} object. The transform function returns a data object,
+ * which can be a modified version of the original data object, or a completely new data
+ * object. The transform can be a function, or an object with a 'fn' key and an optional
+ * 'scope' key. Example usage:
+ *
+ * Ext.create('Ext.data.Store', {
+ * model: 'User',
+ * proxy: {
+ * type: 'ajax',
+ * url : 'users.json',
+ * writer: {
+ * type: 'json',
+ * transform: {
+ * fn: function(data, request) {
+ * // do some manipulation of the unserialized data object
+ * return data;
+ * },
+ * scope: this
+ * }
+ * }
+ * },
+ * });
+ *
+ */
+ transform: null
+ },
+ /**
+ * Creates new Writer.
+ * @param {Object} [config] Config object.
+ */
+ constructor: function(config) {
+ this.initConfig(config);
+ },
+ applyTransform: function(transform) {
+ if (transform) {
+ if (Ext.isFunction(transform)) {
+ transform = {
+ fn: transform
+ };
+ }
+ return transform.fn.bind(transform.scope || this);
+ }
+ return transform;
+ },
+ /**
+ * Prepares a Proxy's Ext.data.Request object.
+ * @param {Ext.data.Request} request The request object.
+ * @return {Ext.data.Request} The modified request object.
+ */
+ write: function(request) {
+ var operation = request.getOperation(),
+ records = operation.getRecords() || [],
+ len = records.length,
+ data = [],
+ i;
+ for (i = 0; i < len; i++) {
+ data.push(this.getRecordData(records[i], operation));
+ }
+ return this.writeRecords(request, data);
+ },
+ /**
+ * @method
+ *
+ * Write the record data to the request in the appropriate format.
+ * @protected
+ * @param {Ext.data.Request} request The request.
+ * @param {Array} data An array of objects containing data.
+ * @return {Ext.data.Request} The request.
+ */
+ writeRecords: Ext.emptyFn,
+ /**
+ * Formats the data for each record before sending it to the server. This method should
+ * be overridden to format the data in a way that differs from the default.
+ *
+ * @param {Ext.data.Model} record The record that we are writing to the server.
+ * @param {Ext.data.operation.Operation} [operation] An operation object.
+ * @return {Object} An object of name/value keys to be written to the server.
+ */
+ getRecordData: function(record, operation) {
+ var me = this,
+ nameProperty = me.getNameProperty(),
+ mapping = nameProperty !== 'name',
+ idField = record.self.idField,
+ key = idField ? (idField[nameProperty] || idField.name) : 'id',
+ value = record.id,
+ writeAll = me.getWriteAllFields(),
+ ret, dateFormat, phantom, options, clientIdProperty, fieldsMap, data, field;
+ if (idField && idField.serialize) {
+ value = idField.serialize(value);
+ }
+ if (!writeAll && operation && operation.isDestroyOperation) {
+ ret = {};
+ ret[key] = value;
+ } else {
+ dateFormat = me.getDateFormat();
+ phantom = record.phantom;
+ options = (phantom || writeAll) ? me.getAllDataOptions() : me.getPartialDataOptions();
+ clientIdProperty = phantom && me.getClientIdProperty();
+ fieldsMap = record.getFieldsMap();
+ options.serialize = false;
+ // we must take over this here
+ data = record.getData(options);
+ // If we are mapping we need to pour data into a new object, otherwise we do
+ // our work in-place:
+ ret = mapping ? {} : data;
+ if (clientIdProperty) {
+ // if (phantom and have clientIdProperty)
+ ret[clientIdProperty] = value;
+ // must read data and write ret
+ delete data[key];
+ }
+ // in case ret === data (must not send "id")
+ else if (!me.getWriteRecordId()) {
+ delete data[key];
+ }
+ for (key in data) {
+ value = data[key];
+ if (!(field = fieldsMap[key])) {
+ // No defined field, so clearly no nameProperty to look up for this field
+ // but if we are mapping we need to copy over the value. Also there is no
+ // serializer to call in this case.
+ if (mapping) {
+ ret[key] = value;
+ }
+ } else {
+ // Allow this Writer to take over formatting date values if it has a
+ // dateFormat specified. Only check isDate on fields declared as dates
+ // for efficiency.
+ if (field.isDateField && dateFormat && Ext.isDate(value)) {
+ value = Ext.Date.format(value, dateFormat);
+ } else if (field.serialize) {
+ value = field.serialize(value, record);
+ }
+ if (mapping) {
+ key = field[nameProperty] || key;
+ }
+ ret[key] = value;
+ }
+ }
+ }
+ return ret;
+ }
+});
+
+/**
+ * Proxies are used by {@link Ext.data.Store Stores} to handle the loading and saving of
+ * {@link Ext.data.Model Model} data. Usually developers will not need to create or interact
+ * with proxies directly.
+ *
+ * # Types of Proxy
+ *
+ * There are two main types of Proxy - {@link Ext.data.proxy.Client Client} and
+ * {@link Ext.data.proxy.Server Server}. The Client proxies save their data locally and include
+ * the following subclasses:
+ *
+ * - {@link Ext.data.proxy.LocalStorage LocalStorageProxy} - saves its data to localStorage
+ * if the browser supports it
+ * - {@link Ext.data.proxy.SessionStorage SessionStorageProxy} - saves its data to sessionStorage
+ * if the browsers supports it
+ * - {@link Ext.data.proxy.Memory MemoryProxy} - holds data in memory only, any data is lost
+ * when the page is refreshed
+ *
+ * The Server proxies save their data by sending requests to some remote server. These proxies
+ * include:
+ *
+ * - {@link Ext.data.proxy.Ajax Ajax} - sends requests to a server on the same domain
+ * - {@link Ext.data.proxy.JsonP JsonP} - uses JSON-P to send requests to a server on a different
+ * domain
+ * - {@link Ext.data.proxy.Rest Rest} - uses RESTful HTTP methods (GET/PUT/POST/DELETE)
+ * to communicate with server
+ * - {@link Ext.data.proxy.Direct Direct} - uses {@link Ext.direct.Manager} to send requests
+ *
+ * Proxies operate on the principle that all operations performed are either Create, Read, Update
+ * or Delete. These four operations are mapped to the methods {@link #method!create},
+ * {@link #method!read}, {@link #method!update} and {@link #method!erase} respectively. Each Proxy
+ * subclass implements these functions.
+ *
+ * The CRUD methods each expect an {@link Ext.data.operation.Operation Operation} object as the only
+ * argument. The Operation encapsulates information about the action the Store wishes to perform,
+ * the {@link Ext.data.Model model} instances that are to be modified, etc. See the
+ * {@link Ext.data.operation.Operation Operation} documentation for more details. Each CRUD
+ * method also accepts a callback function to be called asynchronously on completion.
+ *
+ * Proxies also support batching of Operations via a {@link Ext.data.Batch batch} object, invoked
+ * by the {@link #batch}
+ * method.
+ */
+Ext.define('Ext.data.proxy.Proxy', {
+ mixins: [
+ Ext.mixin.Factoryable,
+ Ext.mixin.Observable
+ ],
+ $configPrefixed: false,
+ alias: 'proxy.proxy',
+ // also configures Factoryable
+ alternateClassName: [
+ 'Ext.data.DataProxy',
+ 'Ext.data.Proxy'
+ ],
+ config: {
+ /**
+ * @cfg {String} batchOrder
+ * Comma-separated ordering 'create', 'update' and 'destroy' actions when batching.
+ * Override this to set a different order for the batched CRUD actions to be executed in.
+ * Defaults to 'create,update,destroy'.
+ */
+ batchOrder: 'create,update,destroy',
+ /**
+ * @cfg {Boolean} batchActions
+ * True to batch actions of a particular type when synchronizing the store. Defaults to
+ * `true`.
+ */
+ batchActions: true,
+ /**
+ * @cfg {String/Ext.data.Model} model
+ * The name of the Model to tie to this Proxy. Can be either the string name of the Model,
+ * or a reference to the Model constructor. Required.
+ */
+ model: undefined,
+ // @cmd-auto-dependency {aliasPrefix : "reader.", defaultPropertyName : "defaultReaderType"}
+ /**
+ * @cfg {Object/String/Ext.data.reader.Reader} reader
+ * The Ext.data.reader.Reader to use to decode the server's response or data read
+ * from client. This can either be a Reader instance, a config object or just a
+ * valid Reader type name (e.g. 'json', 'xml').
+ */
+ reader: {
+ type: 'json'
+ },
+ // @cmd-auto-dependency {aliasPrefix : "writer.", defaultPropertyName : "defaultWriterType"}
+ /**
+ * @cfg {Object/String/Ext.data.writer.Writer} writer
+ * The Ext.data.writer.Writer to use to encode any request sent to the server or
+ * saved to client. This can either be a Writer instance, a config object or just
+ * a valid Writer type name (e.g. 'json', 'xml').
+ */
+ writer: {
+ type: 'json'
+ }
+ },
+ /**
+ * @property {Boolean} isProxy
+ * `true` in this class to identify an object as an instantiated Proxy, or subclass thereof.
+ */
+ isProxy: true,
+ /**
+ * @property {Boolean} [isSynchronous=false]
+ * Identifies the proxy as (a)synchronous.
+ */
+ isSynchronous: false,
+ /**
+ * @event metachange
+ * Fires when this proxy's reader provides new metadata. Metadata usually consists
+ * of new field definitions, but can include any configuration data required by an
+ * application, and can be processed as needed in the event handler.
+ * This event is currently only fired for JsonReaders. Note that this event is also
+ * propagated by {@link Ext.data.Store}, which is typically where it would be handled.
+ * @param {Ext.data.proxy.Proxy} this
+ * @param {Object} meta The JSON metadata
+ */
+ /**
+ * Creates the Proxy
+ * @param {Object} [config] Config object.
+ */
+ constructor: function(config) {
+ // Will call initConfig
+ this.mixins.observable.constructor.call(this, config);
+ // We need to abort all pending operations when destroying
+ this.pendingOperations = {};
+ },
+ applyModel: function(model) {
+ return Ext.data.schema.Schema.lookupEntity(model);
+ },
+ updateModel: function(model) {
+ var reader;
+ if (model) {
+ reader = this.getReader();
+ if (reader && !reader.getModel()) {
+ reader.setModel(model);
+ }
+ }
+ },
+ applyReader: function(reader) {
+ // Synchronous proxies need to force keepRawData to allow Grid features
+ // like Summary and Grouping access rawData after the Reader processed records.
+ // It doesn't do much harm since synchronous proxies are Client side ones,
+ // which will keep their datasets in memory or local storage anyway.
+ if (this.isSynchronous) {
+ reader = reader || {};
+ reader.keepRawData = true;
+ }
+ return Ext.Factory.reader(reader);
+ },
+ updateReader: function(reader) {
+ var me = this,
+ model;
+ if (reader) {
+ model = me.getModel();
+ if (!model) {
+ model = reader.getModel();
+ if (model) {
+ me.setModel(model);
+ }
+ } else {
+ reader.setModel(model);
+ }
+ if (reader.responseType != null) {
+ me.responseType = reader.responseType;
+ }
+ }
+ },
+ applyWriter: function(writer) {
+ var reader = this.getReader();
+ writer = Ext.Factory.writer(writer);
+ // XML Writers may have a record config to define the node name of each record tag.
+ // If not set, but the Reader has a record config, use the Reader's record config.
+ if (writer.getRecord && !writer.getRecord() && reader && reader.getRecord) {
+ reader = reader.getRecord();
+ if (reader) {
+ writer.setRecord(reader);
+ }
+ }
+ return writer;
+ },
+ abort: Ext.emptyFn,
+ /**
+ * @private
+ * Called each time the reader's onMetaChange is called so that the proxy can fire the
+ * metachange event
+ */
+ onMetaChange: function(meta) {
+ this.fireEvent('metachange', this, meta);
+ },
+ /**
+ * Performs the given create operation.
+ * @param {Ext.data.operation.Operation} operation The Operation to perform
+ * @method
+ */
+ create: Ext.emptyFn,
+ /**
+ * Performs the given read operation.
+ * @param {Ext.data.operation.Operation} operation The Operation to perform
+ * @method
+ */
+ read: Ext.emptyFn,
+ /**
+ * Performs the given update operation.
+ * @param {Ext.data.operation.Operation} operation The Operation to perform
+ * @method
+ */
+ update: Ext.emptyFn,
+ /**
+ * Performs the given destroy operation.
+ * @param {Ext.data.operation.Operation} operation The Operation to perform
+ * @method
+ */
+ erase: Ext.emptyFn,
+ /**
+ * Performs a batch of {@link Ext.data.operation.Operation Operations}, in the order specified
+ * by {@link #batchOrder}. Used internally by {@link Ext.data.Store}'s
+ * {@link Ext.data.Store#sync sync} method. Example usage:
+ *
+ * myProxy.batch({
+ * create : [myModel1, myModel2],
+ * update : [myModel3],
+ * destroy: [myModel4, myModel5]
+ * });
+ *
+ * Where the myModel* above are {@link Ext.data.Model Model} instances - in this case 1 and 2
+ * are new instances and have not been saved before, 3 has been saved previously but needs to be
+ * updated, and 4 and 5 have already been saved but should now be destroyed.
+ *
+ * Note that the previous version of this method took 2 arguments (operations and listeners).
+ * While this is still supported for now, the current signature is now a single `options`
+ * argument that can contain both operations and listeners, in addition to other options.
+ * The multi-argument signature will likely be deprecated in a future release.
+ *
+ * @param {Object} options Object containing one or more properties supported by the batch
+ * method:
+ *
+ * @param {Object} options.operations Object containing the Model instances to act upon, keyed
+ * by action name
+ *
+ * @param {Object} [options.listeners] Event listeners object passed straight through to the
+ * Batch - see {@link Ext.data.Batch} for details
+ *
+ * @param {Ext.data.Batch/Object} [options.batch] A {@link Ext.data.Batch} object (or batch
+ * config to apply to the created batch). If unspecified a default batch will be auto-created.
+ *
+ * @param {Function} [options.callback] The function to be called upon completion of processing
+ * the batch. The callback is called regardless of success or failure and is passed the
+ * following parameters:
+ * @param {Ext.data.Batch} options.callback.batch The {@link Ext.data.Batch batch} that was
+ * processed, containing all operations in their current state after processing
+ * @param {Object} options.callback.options The options argument that was originally passed
+ * into batch
+ *
+ * @param {Function} [options.success] The function to be called upon successful completion
+ * of the batch. The success function is called only if no exceptions were reported in any
+ * operations. If one or more exceptions occurred then the `failure` function will be called
+ * instead. The success function is called with the following parameters:
+ * @param {Ext.data.Batch} options.success.batch The {@link Ext.data.Batch batch} that was
+ * processed, containing all operations in their current state after processing
+ * @param {Object} options.success.options The options argument that was originally passed into
+ * batch
+ *
+ * @param {Function} [options.failure] The function to be called upon unsuccessful completion
+ * of the batch. The failure function is called when one or more operations returns an
+ * exception during processing (even if some operations were also successful). In this case you
+ * can check the batch's {@link Ext.data.Batch#exceptions exceptions} array to see exactly
+ * which operations had exceptions. The failure function is called with the following
+ * parameters:
+ * @param {Ext.data.Batch} options.failure.batch The {@link Ext.data.Batch batch} that was
+ * processed, containing all operations in their current state after processing
+ * @param {Object} options.failure.options The options argument that was originally passed into
+ * batch
+ *
+ * @param {Object} [options.scope] The scope in which to execute any callbacks (i.e. the `this`
+ * object inside the callback, success and/or failure functions). Defaults to the proxy.
+ *
+ * @param {Object} [listeners] (deprecated) If `options` is the `operations`, this
+ * parameter is the listeners. Instead of passing these two arguments, the proper form
+ * is to pass them as:
+ *
+ * batch({
+ * operations: ...
+ * listeners: ...
+ * });
+ *
+ * @return {Ext.data.Batch} The newly created Batch
+ */
+ batch: function(options, listeners) {
+ var me = this,
+ useBatch = me.getBatchActions(),
+ batch, records, actions, aLen, action, a, r, rLen, record;
+ if (options.operations === undefined) {
+ // the old-style (operations, listeners) signature was called
+ // so convert to the single options argument syntax
+ options = {
+ operations: options,
+ listeners: listeners
+ };
+ }
+ if (options.batch) {
+ if (Ext.isDefined(options.batch.runOperation)) {
+ batch = Ext.applyIf(options.batch, {
+ proxy: me,
+ listeners: {}
+ });
+ }
+ } else {
+ options.batch = {
+ proxy: me,
+ listeners: options.listeners || {}
+ };
+ }
+ if (!batch) {
+ batch = new Ext.data.Batch(options.batch);
+ }
+ // Use single so that the listener gets removed upon completion.
+ batch.on('complete', Ext.bind(me.onBatchComplete, me, [
+ options
+ ], 0), null, {
+ single: true,
+ priority: 1000
+ });
+ batch.$destroyOwner = options.$destroyOwner;
+ actions = me.getBatchOrder().split(',');
+ aLen = actions.length;
+ for (a = 0; a < aLen; a++) {
+ action = actions[a];
+ records = options.operations[action];
+ if (records) {
+ if (useBatch) {
+ batch.add(me.createOperation(action, {
+ records: records,
+ // Relay any additional params through to the Operation (and Request).
+ params: options.params
+ }));
+ } else {
+ rLen = records.length;
+ for (r = 0; r < rLen; r++) {
+ record = records[r];
+ batch.add(me.createOperation(action, {
+ records: [
+ record
+ ],
+ // Relay any additional params through to the Operation (and Request).
+ params: options.params
+ }));
+ }
+ }
+ }
+ }
+ batch.start();
+ return batch;
+ },
+ /**
+ * @private
+ * The internal callback that the proxy uses to call any specified user callbacks after
+ * completion of a batch
+ */
+ onBatchComplete: function(batchOptions, batch) {
+ var scope = batchOptions.scope || this;
+ if (batch.hasException()) {
+ if (Ext.isFunction(batchOptions.failure)) {
+ Ext.callback(batchOptions.failure, scope, [
+ batch,
+ batchOptions
+ ]);
+ }
+ } else if (Ext.isFunction(batchOptions.success)) {
+ Ext.callback(batchOptions.success, scope, [
+ batch,
+ batchOptions
+ ]);
+ }
+ if (Ext.isFunction(batchOptions.callback)) {
+ Ext.callback(batchOptions.callback, scope, [
+ batch,
+ batchOptions
+ ]);
+ }
+ // In certain cases when the batch was created by a ProxyStore we need to
+ // defer destruction until the store can process the batch results.
+ // The store will then destroy the batch.
+ if (!batch.$destroyOwner) {
+ batch.destroy();
+ }
+ },
+ createOperation: function(action, config) {
+ var operation = Ext.createByAlias('data.operation.' + action, config);
+ operation.setProxy(this);
+ this.pendingOperations[operation._internalId] = operation;
+ return operation;
+ },
+ completeOperation: function(operation) {
+ delete this.pendingOperations[operation._internalId];
+ },
+ clone: function() {
+ return new this.self(this.getInitialConfig());
+ },
+ destroy: function() {
+ var ops = this.pendingOperations,
+ opId, op;
+ for (opId in ops) {
+ op = ops[opId];
+ if (op && op.isRunning()) {
+ op.abort();
+ }
+ op.destroy();
+ }
+ this.pendingOperations = null;
+ this.callParent();
+ }
+});
+
+/**
+ * Base class for any client-side storage. Used as a superclass for
+ * {@link Ext.data.proxy.Memory Memory} and {@link Ext.data.proxy.WebStorage Web Storage} proxies.
+ * Do not use directly, use one of the subclasses instead.
+ * @private
+ */
+Ext.define('Ext.data.proxy.Client', {
+ extend: Ext.data.proxy.Proxy,
+ alternateClassName: 'Ext.data.ClientProxy',
+ /**
+ * @property {Boolean} isSynchronous
+ * `true` in this class to identify that requests made on this proxy are
+ * performed synchronously
+ */
+ isSynchronous: true,
+ /**
+ * Abstract function that must be implemented by each ClientProxy subclass. This should purge
+ * all record data from the client side storage, as well as removing any supporting data
+ * (such as lists of record IDs)
+ */
+ clear: function() {
+ Ext.raise("The Ext.data.proxy.Client subclass that you are using has not defined " + "a 'clear' function. See src/data/ClientProxy.js for details.");
+ }
+});
+
+/**
+ * In-memory proxy. This proxy simply uses a local variable for data storage/retrieval, so its
+ * contents are lost on every page refresh.
+ *
+ * Usually this Proxy isn't used directly, serving instead as a helper to a
+ * {@link Ext.data.Store Store} where a reader is required to load data. For example, say we have
+ * a Store for a User model and have some inline data we want to load, but this data isn't in quite
+ * the right format: we can use a MemoryProxy with a JsonReader to read it into our Store:
+ *
+ * // this is the model we will be using in the store
+ * Ext.define('User', {
+ * extend: 'Ext.data.Model',
+ * fields: [
+ * { name: 'id', type: 'int' },
+ * { name: 'name', type: 'string' },
+ * { name: 'phone', type: 'string', mapping: 'phoneNumber' }
+ * ]
+ * });
+ *
+ * // this data does not line up to our model fields - the phone field is called phoneNumber
+ * var data = {
+ * users: [
+ * {
+ * id: 1,
+ * name: 'Ed Spencer',
+ * phoneNumber: '555 1234'
+ * },
+ * {
+ * id: 2,
+ * name: 'Abe Elias',
+ * phoneNumber: '666 1234'
+ * }
+ * ]
+ * };
+ *
+ * // note how we set the 'root' in the reader to match the data structure above
+ * var store = Ext.create('Ext.data.Store', {
+ * autoLoad: true,
+ * model: 'User',
+ * data: data,
+ * proxy: {
+ * type: 'memory',
+ * reader: {
+ * type: 'json',
+ * rootProperty: 'users'
+ * }
+ * }
+ * });
+ */
+Ext.define('Ext.data.proxy.Memory', {
+ extend: Ext.data.proxy.Client,
+ alias: 'proxy.memory',
+ alternateClassName: 'Ext.data.MemoryProxy',
+ isMemoryProxy: true,
+ config: {
+ /**
+ * @cfg {Boolean} [enablePaging=false]
+ * Configure as `true` to enable this MemoryProxy to honour a read operation's `start`
+ * and `limit` options.
+ *
+ * When `true`, read operations will be able to read *pages* of records from the data object.
+ */
+ enablePaging: null,
+ /**
+ * @cfg {Object} data
+ * Optional data to pass to configured Reader.
+ */
+ data: {
+ $value: null,
+ // Because of destructive association reading, we always need to clone incoming data
+ // to protect externally owned data objects from mutation
+ merge: function(newValue, currentValue, target, mixinClass) {
+ return newValue ? Ext.clone(newValue) : newValue;
+ }
+ },
+ /**
+ * @cfg {Boolean} [clearOnRead=false]
+ * By default MemoryProxy data is persistent, and subsequent reads will read the
+ * same data. If this is not required, configure the proxy using `clearOnRead: true`.
+ */
+ clearOnRead: null
+ },
+ /**
+ * @private
+ * Fake processing function to commit the records, set the current operation
+ * to successful and call the callback if provided. This function is shared
+ * by the create, update and destroy methods to perform the bare minimum
+ * processing required for the proxy to register a result from the action.
+ */
+ finishOperation: function(operation) {
+ var recs = operation.getRecords(),
+ len = recs.length,
+ i;
+ for (i = 0; i < len; i++) {
+ // Because Memory proxy is synchronous, the commit must call store#afterErase
+ recs[i].dropped = !!operation.isDestroyOperation;
+ recs[i].commit();
+ }
+ operation.setSuccessful(true);
+ },
+ /**
+ * Currently this is a hard-coded method that simply commits any records and sets the operation
+ * to successful, then calls the callback function, if provided. It is essentially mocking
+ * a server call in memory, but since there is no real back end in this case there's not much
+ * else to do. This method can be easily overridden to implement more complex logic if needed.
+ * @param {Ext.data.operation.Operation} operation The Operation to perform
+ * @method
+ */
+ create: function(operation) {
+ this.finishOperation(operation);
+ },
+ /**
+ * Currently this is a hard-coded method that simply commits any records and sets the operation
+ * to successful, then calls the callback function, if provided. It is essentially mocking
+ * a server call in memory, but since there is no real back end in this case there's not much
+ * else to do. This method can be easily overridden to implement more complex logic if needed.
+ * @param {Ext.data.operation.Operation} operation The Operation to perform
+ * @method
+ */
+ update: function(operation) {
+ this.finishOperation(operation);
+ },
+ /**
+ * Currently this is a hard-coded method that simply commits any records and sets the operation
+ * to successful, then calls the callback function, if provided. It is essentially mocking
+ * a server call in memory, but since there is no real back end in this case there's not much
+ * else to do. This method can be easily overridden to implement more complex logic if needed.
+ * @param {Ext.data.operation.Operation} operation The Operation to perform
+ * @method
+ */
+ erase: function(operation) {
+ this.finishOperation(operation);
+ },
+ /**
+ * Reads data from the configured {@link #data} object. Uses the Proxy's {@link #reader},
+ * if present.
+ * @param {Ext.data.operation.Operation} operation The read Operation
+ */
+ read: function(operation) {
+ var me = this,
+ reader = me.getReader(),
+ resultSet = reader.read(me.getData(), {
+ recordCreator: reader.defaultRecordCreatorFromServer
+ }),
+ records = resultSet.getRecords(),
+ sorters = operation.getSorters(),
+ grouper = operation.getGrouper(),
+ filters = operation.getFilters(),
+ start = operation.getStart(),
+ limit = operation.getLimit(),
+ meta;
+ // Apply filters, sorters, and start/limit options
+ if (operation.process(resultSet, null, null, false) !== false) {
+ // If we are configured to read the data one time only, clear our data
+ if (operation.success && me.getClearOnRead()) {
+ this.setData(null);
+ }
+ // Filter the resulting array of records
+ if (filters && filters.length) {
+ // Total will be updated by setting records
+ /* eslint-disable-next-line max-len */
+ resultSet.setRecords(records = Ext.Array.filter(records, Ext.util.Filter.createFilterFn(filters)));
+ resultSet.setTotal(records.length);
+ }
+ // Remotely, grouper just mean top priority sorters
+ if (grouper) {
+ // Must concat so as not to mutate passed sorters array which could be the items
+ // property of the sorters collection
+ sorters = sorters ? sorters.concat(grouper) : sorters;
+ }
+ // Sort by the specified grouper and sorters
+ if (sorters && sorters.length) {
+ /* eslint-disable-next-line max-len */
+ resultSet.setRecords(records = Ext.Array.sort(records, Ext.util.Sortable.createComparator(sorters)));
+ }
+ // Reader reads the whole passed data object.
+ // If successful and we were given a start and limit, slice the result.
+ if (me.getEnablePaging() && start !== undefined && limit !== undefined) {
+ // Attempt to read past end of memory dataset - convert to failure
+ if (start >= resultSet.getTotal()) {
+ resultSet.setConfig({
+ success: false,
+ records: [],
+ total: 0
+ });
+ } else // Range is valid, slice it up.
+ {
+ resultSet.setRecords(Ext.Array.slice(records, start, start + limit));
+ }
+ }
+ operation.setCompleted();
+ // If a JsonReader detected metadata, process it now.
+ // This will fire the 'metachange' event which the Store processes to fire its own
+ // 'metachange'
+ meta = resultSet.getMetadata();
+ if (meta) {
+ me.onMetaChange(meta);
+ }
+ }
+ },
+ clear: Ext.emptyFn
+});
+
+/**
+ * ProxyStore is a superclass of {@link Ext.data.Store} and {@link Ext.data.BufferedStore}.
+ * It's never used directly, but offers a set of methods used by both of those subclasses.
+ *
+ * We've left it here in the docs for reference purposes, but unless you need to make a whole new
+ * type of Store, what you're probably looking for is {@link Ext.data.Store}. If you're still
+ * interested, here's a brief description of what ProxyStore is and is not.
+ *
+ * ProxyStore provides the basic configuration for anything that can be considered a Store.
+ * It expects to be given a {@link Ext.data.Model Model} that represents the type of data
+ * in the Store. It also expects to be given a {@link Ext.data.proxy.Proxy Proxy} that handles
+ * the loading of data into the Store.
+ *
+ * ProxyStore provides a few helpful methods such as {@link #method-load} and {@link #sync},
+ * which load and save data respectively, passing the requests through the configured
+ * {@link #proxy}.
+ *
+ * Built-in Store subclasses add extra behavior to each of these functions. Note also that each
+ * ProxyStore subclass has its own way of storing data - in {@link Ext.data.Store} the data
+ * is saved as a flat {@link Ext.util.Collection Collection}, whereas in
+ * {@link Ext.data.BufferedStore BufferedStore} we use a {@link Ext.data.PageMap} to maintain
+ * a client side cache of pages of records.
+ *
+ * The store provides filtering and sorting support. This sorting/filtering can happen on the
+ * client side or can be completed on the server. This is controlled by the
+ * {@link Ext.data.Store#remoteSort remoteSort} and {@link Ext.data.Store#remoteFilter remoteFilter}
+ * config options. For more information see the {@link #method-sort} and
+ * {@link Ext.data.Store#filter filter} methods.
+ */
+Ext.define('Ext.data.ProxyStore', {
+ extend: Ext.data.AbstractStore,
+ config: {
+ // @cmd-auto-dependency {aliasPrefix: "model.", mvc: true, blame: "all"}
+ /**
+ * @cfg {String/Ext.data.Model} model
+ * Name of the {@link Ext.data.Model Model} associated with this store. See
+ * {@link Ext.data.Model#entityName}.
+ *
+ * May also be the actual Model subclass.
+ *
+ * This config is required for the store to be able to read data unless you have
+ * defined the {@link #fields} config which will create an anonymous
+ * `Ext.data.Model`.
+ */
+ model: undefined,
+ // @cmd-auto-dependency {aliasPrefix: "data.field."}
+ /**
+ * @cfg fields
+ * @inheritdoc Ext.data.Model#cfg-fields
+ *
+ * @localdoc **Note:** In general, this configuration option should only be used
+ * for simple stores like a two-field store of
+ * {@link Ext.form.field.ComboBox ComboBox}. For anything more complicated, such
+ * as specifying a particular id property or associations, a
+ * {@link Ext.data.Model Model} should be defined and specified for the
+ * {@link #model} config.
+ *
+ * @since 2.3.0
+ */
+ fields: null,
+ // @cmd-auto-dependency {aliasPrefix : "proxy."}
+ /**
+ * @cfg {String/Ext.data.proxy.Proxy/Object} proxy
+ * The Proxy to use for this Store. This can be either a string, a config object
+ * or a Proxy instance - see {@link #setProxy} for details.
+ * @since 1.1.0
+ */
+ proxy: undefined,
+ /**
+ * @cfg {Boolean/Object} autoLoad
+ * If data is not specified, and if autoLoad is true or an Object, this store's
+ * load method is automatically called after creation. If the value of autoLoad
+ * is an Object, this Object will be passed to the store's load method.
+ *
+ * It's important to note that {@link Ext.data.TreeStore Tree Stores} will
+ * load regardless of autoLoad's value if expand is set to true on the
+ * {@link Ext.data.TreeStore#root root node}.
+ *
+ * @since 2.3.0
+ */
+ autoLoad: undefined,
+ /**
+ * @cfg {Boolean} autoSync
+ * True to automatically sync the Store with its Proxy after every edit to one of
+ * its Records. Defaults to false.
+ */
+ autoSync: false,
+ /**
+ * @cfg {String} batchUpdateMode
+ * Sets the updating behavior based on batch synchronization. 'operation' (the
+ * default) will update the Store's internal representation of the data after
+ * each operation of the batch has completed, 'complete' will wait until the
+ * entire batch has been completed before updating the Store's data. 'complete'
+ * is a good choice for local storage proxies, 'operation' is better for remote
+ * proxies, where there is a comparatively high latency.
+ */
+ batchUpdateMode: 'operation',
+ /**
+ * @cfg {Boolean} sortOnLoad
+ * If true, any sorters attached to this Store will be run after loading data,
+ * before the datachanged event is fired. Defaults to true, ignored if
+ * {@link Ext.data.Store#remoteSort remoteSort} is true
+ */
+ sortOnLoad: true,
+ /**
+ * @cfg {Boolean} trackRemoved
+ * This config controls whether removed records are remembered by this store for
+ * later saving to the server.
+ */
+ trackRemoved: true,
+ /**
+ * @cfg {Boolean} asynchronousLoad
+ * This defaults to `true` when this store's {@link #cfg-proxy} is asynchronous,
+ * such as an {@link Ext.data.proxy.Ajax Ajax proxy}.
+ *
+ * When the proxy is synchronous, such as a {@link Ext.data.proxy.Memory} memory
+ * proxy, this defaults to `false`.
+ *
+ * *NOTE:* This does not cause synchronous Ajax requests if configured `false`
+ * when an Ajax proxy is used. It causes immediate issuing of an Ajax request
+ * when {@link #method-load} is called rather than issuing the request at the end
+ * of the current event handler run.
+ *
+ * What this means is that when using an Ajax proxy, calls to
+ * {@link #method-load} do not fire the request to the remote resource
+ * immediately, but schedule a request to be made. This is so that multiple
+ * requests are not fired when mutating a store's remote filters and sorters (as
+ * happens during state restoration). The request is made only once after all
+ * relevant store state is fully set.
+ *
+ * @since 6.0.1
+ */
+ asynchronousLoad: undefined
+ },
+ onClassExtended: function(cls, data, hooks) {
+ var model = data.model,
+ onBeforeClassCreated;
+ if (typeof model === 'string') {
+ onBeforeClassCreated = hooks.onBeforeCreated;
+ hooks.onBeforeCreated = function() {
+ var me = this,
+ args = arguments;
+ Ext.require(model, function() {
+ onBeforeClassCreated.apply(me, args);
+ });
+ };
+ }
+ },
+ /**
+ * @private
+ * @property {Boolean} implicitModel
+ * The class name of the model that this store uses if no explicit {@link #model} is
+ * given
+ */
+ implicitModel: 'Ext.data.Model',
+ /**
+ * @property {Object} lastOptions
+ * Property to hold the last options from a {@link #method-load} method call. This
+ * object is used for the {@link #method-reload} to reuse the same options. Please
+ * see {@link #method-reload} for a simple example on how to use the lastOptions
+ * property.
+ */
+ /**
+ * @property {Number} autoSyncSuspended
+ * A counter to track suspensions.
+ * @private
+ */
+ autoSyncSuspended: 0,
+ /**
+ * @property {Ext.data.Model[]} removed
+ * Temporary cache in which removed model instances are kept until successfully
+ * synchronised with a Proxy, at which point this is cleared.
+ *
+ * This cache is maintained unless you set `trackRemoved` to `false`.
+ *
+ * @protected
+ * @readonly
+ */
+ removed: null,
+ /**
+ * @event beforeload
+ * Fires before a request is made for a new data object. If the beforeload handler returns
+ * `false` the load action will be canceled.
+ *
+ * **Note:** If you are using a buffered store, you should use
+ * {@link Ext.data.Store#beforeprefetch beforeprefetch}.
+ * @param {Ext.data.Store} store This Store
+ * @param {Ext.data.operation.Operation} operation The Ext.data.operation.Operation object
+ * that will be passed to the Proxy to load the Store
+ * @since 1.1.0
+ */
+ /**
+ * @event load
+ * Fires whenever the store reads data from a remote data source.
+ *
+ * **Note:** If you are using a buffered store, you should use
+ * {@link Ext.data.Store#prefetch prefetch}.
+ * @param {Ext.data.Store} this
+ * @param {Ext.data.Model[]} records An array of records
+ * @param {Boolean} successful True if the operation was successful.
+ * @param {Ext.data.operation.Read} operation The
+ * {@link Ext.data.operation.Read Operation} object that was used in the data
+ * load call
+ * @since 1.1.0
+ */
+ /**
+ * @event write
+ * Fires whenever a successful write has been made via the configured {@link #proxy Proxy}
+ * @param {Ext.data.Store} store This Store
+ * @param {Ext.data.operation.Operation} operation The
+ * {@link Ext.data.operation.Operation Operation} object that was used in the write
+ * @since 3.4.0
+ */
+ /**
+ * @event beforesync
+ * Fired before a call to {@link #sync} is executed. Return false from any listener to cancel
+ * the sync
+ * @param {Object} options Hash of all records to be synchronized, broken down into create,
+ * update and destroy
+ */
+ /**
+ * @event metachange
+ * Fires when this store's underlying reader (available via the proxy) provides new metadata.
+ * Metadata usually consists of new field definitions, but can include any configuration data
+ * required by an application, and can be processed as needed in the event handler.
+ * This event is currently only fired for JsonReaders.
+ * @param {Ext.data.Store} this
+ * @param {Object} meta The JSON metadata
+ * @since 1.1.0
+ */
+ constructor: function(config) {
+ var me = this;
+ var configModel = me.model;
+ // eslint-disable-line vars-on-top, one-var
+ me.callParent(arguments);
+ if (me.getAsynchronousLoad() === false) {
+ me.flushLoad();
+ }
+ if (!me.getModel() && me.useModelWarning !== false && me.getStoreId() !== 'ext-empty-store') {
+ // There are a number of ways things could have gone wrong, try to give as much
+ // information as possible
+ var logMsg = [
+ // eslint-disable-line vars-on-top, one-var
+ Ext.getClassName(me) || 'Store',
+ ' created with no model.'
+ ];
+ if (typeof configModel === 'string') {
+ logMsg.push(" The name '", configModel, "'", ' does not correspond to a valid model.');
+ }
+ Ext.log.warn(logMsg.join(''));
+ }
+ },
+ /**
+ * @private
+ */
+ doDestroy: function() {
+ var me = this,
+ proxy = me.getProxy();
+ me.clearLoadTask();
+ Ext.destroy(me.getData());
+ me.data = null;
+ me.setProxy(null);
+ if (proxy.autoCreated) {
+ proxy.destroy();
+ }
+ me.setModel(null);
+ me.callParent();
+ },
+ applyAsynchronousLoad: function(asynchronousLoad) {
+ // Default in an asynchronousLoad setting.
+ // It defaults to false if the proxy is synchronous, and true if the proxy is asynchronous.
+ if (asynchronousLoad == null) {
+ asynchronousLoad = !this.loadsSynchronously();
+ }
+ return asynchronousLoad;
+ },
+ updateAutoLoad: function(autoLoad) {
+ // Ensure the data collection is set up
+ this.getData();
+ if (autoLoad) {
+ // Defer the load until idle, when the store (and probably the view)
+ // is fully constructed
+ this.load(Ext.isObject(autoLoad) ? autoLoad : undefined);
+ }
+ },
+ /**
+ * Returns the total number of {@link Ext.data.Model Model} instances that the
+ * {@link Ext.data.proxy.Proxy Proxy} indicates exist. This will usually differ from
+ * {@link #getCount} when using paging - getCount returns the number of records loaded into
+ * the Store at the moment, getTotalCount returns the number of records that could be loaded
+ * into the Store if the Store contained all data
+ * @return {Number} The total number of Model instances available via the Proxy. 0 returned if
+ * no value has been set via the reader.
+ */
+ getTotalCount: function() {
+ return this.totalCount || 0;
+ },
+ applyFields: function(fields) {
+ if (fields) {
+ this.createImplicitModel(fields);
+ }
+ },
+ applyModel: function(model) {
+ if (model) {
+ model = Ext.data.schema.Schema.lookupEntity(model);
+ } else if (!this.destroying) {
+ // If no model, ensure that the fields config is converted to a model.
+ this.getFields();
+ model = this.getModel() || this.createImplicitModel();
+ }
+ return model;
+ },
+ applyProxy: function(proxy) {
+ var model = this.getModel();
+ if (proxy !== null) {
+ if (proxy) {
+ if (proxy.isProxy) {
+ proxy.setModel(model);
+ } else {
+ if (Ext.isString(proxy)) {
+ proxy = {
+ type: proxy,
+ model: model
+ };
+ } else if (!proxy.model) {
+ proxy = Ext.apply({
+ model: model
+ }, proxy);
+ }
+ proxy = Ext.createByAlias('proxy.' + proxy.type, proxy);
+ proxy.autoCreated = true;
+ }
+ } else if (model) {
+ proxy = model.getProxy();
+ this.useModelProxy = true;
+ }
+ if (!proxy) {
+ proxy = Ext.createByAlias('proxy.memory');
+ proxy.autoCreated = true;
+ }
+ }
+ return proxy;
+ },
+ applyState: function(state) {
+ var me = this;
+ me.callParent([
+ state
+ ]);
+ // This is called during construction. Sorters and filters might have changed
+ // which require a reload.
+ // If autoLoad is true, it might have loaded synchronously from a memory proxy,
+ // so needs to reload.
+ // If it is already loaded, we definitely need to reload to apply the state.
+ if (me.getAutoLoad() || me.isLoaded()) {
+ me.load();
+ }
+ },
+ updateProxy: function(proxy, oldProxy) {
+ this.proxyListeners = Ext.destroy(this.proxyListeners);
+ },
+ updateTrackRemoved: function(track) {
+ this.cleanRemoved();
+ this.removed = track ? [] : null;
+ },
+ /**
+ * @private
+ */
+ onMetaChange: function(proxy, meta) {
+ this.fireEvent('metachange', this, meta);
+ },
+ // saves any phantom records
+ create: function(data, options) {
+ var me = this,
+ Model = me.getModel(),
+ instance = new Model(data),
+ operation;
+ options = Ext.apply({}, options);
+ if (!options.records) {
+ options.records = [
+ instance
+ ];
+ }
+ options.internalScope = me;
+ options.internalCallback = me.onProxyWrite;
+ operation = me.createOperation('create', options);
+ return operation.execute();
+ },
+ read: function() {
+ return this.load.apply(this, arguments);
+ },
+ update: function(options) {
+ var me = this,
+ operation;
+ options = Ext.apply({}, options);
+ if (!options.records) {
+ options.records = me.getUpdatedRecords();
+ }
+ options.internalScope = me;
+ options.internalCallback = me.onProxyWrite;
+ operation = me.createOperation('update', options);
+ return operation.execute();
+ },
+ /**
+ * @private
+ * Callback for any write Operation over the Proxy. Updates the Store's MixedCollection
+ * to reflect the updates provided by the Proxy
+ */
+ onProxyWrite: function(operation) {
+ var me = this,
+ success = operation.wasSuccessful(),
+ records = operation.getRecords();
+ switch (operation.getAction()) {
+ case 'create':
+ me.onCreateRecords(records, operation, success);
+ break;
+ case 'update':
+ me.onUpdateRecords(records, operation, success);
+ break;
+ case 'destroy':
+ me.onDestroyRecords(records, operation, success);
+ break;
+ }
+ if (success) {
+ me.fireEvent('write', me, operation);
+ me.fireEvent('datachanged', me);
+ }
+ },
+ // may be implemented by store subclasses
+ onCreateRecords: Ext.emptyFn,
+ // may be implemented by store subclasses
+ onUpdateRecords: Ext.emptyFn,
+ /**
+ * Removes any records when a write is returned from the server.
+ * @private
+ * @param {Ext.data.Model[]} records The array of removed records
+ * @param {Ext.data.operation.Operation} operation The operation that just completed
+ * @param {Boolean} success True if the operation was successful
+ */
+ onDestroyRecords: function(records, operation, success) {
+ if (success) {
+ this.cleanRemoved();
+ }
+ },
+ // tells the attached proxy to destroy the given records
+ // @since 3.4.0
+ erase: function(options) {
+ var me = this,
+ operation;
+ options = Ext.apply({}, options);
+ if (!options.records) {
+ options.records = me.getRemovedRecords();
+ }
+ options.internalScope = me;
+ options.internalCallback = me.onProxyWrite;
+ operation = me.createOperation('destroy', options);
+ return operation.execute();
+ },
+ /**
+ * @private
+ * Attached as the 'operationcomplete' event listener to a proxy's Batch object. By default
+ * just calls through to onProxyWrite.
+ */
+ onBatchOperationComplete: function(batch, operation) {
+ return this.onProxyWrite(operation);
+ },
+ /**
+ * @private
+ * Attached as the 'complete' event listener to a proxy's Batch object. Iterates over the batch
+ * operations and updates the Store's internal data MixedCollection.
+ */
+ onBatchComplete: function(batch, operation) {
+ var me = this,
+ operations = batch.operations,
+ length = operations.length,
+ i;
+ if (me.batchUpdateMode !== 'operation') {
+ me.suspendEvents();
+ for (i = 0; i < length; i++) {
+ me.onProxyWrite(operations[i]);
+ }
+ me.resumeEvents();
+ }
+ me.isSyncing = false;
+ if (batch.$destroyOwner === me) {
+ batch.destroy();
+ }
+ me.fireEvent('datachanged', me);
+ },
+ /**
+ * @private
+ */
+ onBatchException: function(batch, operation) {},
+ // //decide what to do... could continue with the next operation
+ // batch.start();
+ //
+ // //or retry the last operation
+ // batch.retry();
+ /**
+ * @private
+ * Filter function for new records.
+ */
+ filterNew: function(item) {
+ // only want phantom records that are valid
+ return item.phantom && item.isValid();
+ },
+ /**
+ * Returns all `{@link Ext.data.Model#property-phantom phantom}` records in this store.
+ * @return {Ext.data.Model[]} A possibly empty array of `phantom` records.
+ */
+ getNewRecords: function() {
+ return [];
+ },
+ /**
+ * Returns all valid, non-phantom Model instances that have been updated in the Store but
+ * not yet synchronized with the Proxy.
+ * @return {Ext.data.Model[]} The updated Model instances
+ */
+ getUpdatedRecords: function() {
+ return [];
+ },
+ /**
+ * Gets all {@link Ext.data.Model records} added or updated since the last commit. Note that
+ * the order of records returned is not deterministic and does not indicate the order in which
+ * records were modified. Note also that removed records are not included
+ * (use {@link #getRemovedRecords} for that).
+ * @return {Ext.data.Model[]} The added and updated Model instances
+ */
+ getModifiedRecords: function() {
+ return [].concat(this.getNewRecords(), this.getUpdatedRecords());
+ },
+ /**
+ * @private
+ * Filter function for updated records.
+ */
+ filterUpdated: function(item) {
+ // only want dirty records, not phantoms that are valid
+ return item.dirty && !item.phantom && item.isValid();
+ },
+ /**
+ * Returns any records that have been removed from the store but not yet destroyed on the proxy.
+ * @return {Ext.data.Model[]} The removed Model instances. Note that this is a *copy* of the
+ * store's array, so may be mutated.
+ */
+ getRemovedRecords: function() {
+ var removed = this.getRawRemovedRecords();
+ return removed ? Ext.Array.clone(removed) : [];
+ },
+ /**
+ * Synchronizes the store with its {@link #proxy}. This asks the proxy to batch together any
+ * new, updated and deleted records in the store, updating the store's internal representation
+ * of the records as each operation completes.
+ *
+ * @param {Object} [options] Object containing one or more properties supported by the sync
+ * method (these get passed along to the underlying proxy's {@link Ext.data.Proxy#batch batch}
+ * method):
+ *
+ * @param {Ext.data.Batch/Object} [options.batch] A {@link Ext.data.Batch} object (or batch
+ * config to apply to the created batch). If unspecified a default batch will be auto-created
+ * as needed.
+ *
+ * @param {Function} [options.callback] The function to be called upon completion of the sync.
+ * The callback is called regardless of success or failure and is passed the following
+ * parameters:
+ * @param {Ext.data.Batch} options.callback.batch The {@link Ext.data.Batch batch} that was
+ * processed, containing all operations in their current state after processing
+ * @param {Object} options.callback.options The options argument that was originally passed
+ * into sync
+ *
+ * @param {Function} [options.success] The function to be called upon successful completion
+ * of the sync. The success function is called only if no exceptions were reported in any
+ * operations. If one or more exceptions occurred then the failure function will be called
+ * instead. The success function is called with the following parameters:
+ * @param {Ext.data.Batch} options.success.batch The {@link Ext.data.Batch batch} that was
+ * processed, containing all operations in their current state after processing
+ * @param {Object} options.success.options The options argument that was originally passed
+ * into sync
+ *
+ * @param {Function} [options.failure] The function to be called upon unsuccessful completion
+ * of the sync. The failure function is called when one or more operations returns an exception
+ * during processing (even if some operations were also successful). In this case you can check
+ * the batch's {@link Ext.data.Batch#exceptions exceptions} array to see exactly which
+ * operations had exceptions. The failure function is called with the following parameters:
+ * @param {Ext.data.Batch} options.failure.batch The {@link Ext.data.Batch} that was processed,
+ * containing all operations in their current state after processing
+ * @param {Object} options.failure.options The options argument that was originally passed
+ * into sync
+ *
+ * @param {Object} [options.params] Additional params to send during the sync Operation(s).
+ *
+ * @param {Object} [options.scope] The scope in which to execute any callbacks (i.e. the `this`
+ * object inside the callback, success and/or failure functions). Defaults to the store's proxy.
+ *
+ * @return {Ext.data.Store} this
+ */
+ sync: function(options) {
+ var me = this,
+ operations = {},
+ toCreate = me.getNewRecords(),
+ toUpdate = me.getUpdatedRecords(),
+ toDestroy = me.getRemovedRecords(),
+ needsSync = false;
+ if (me.isSyncing) {
+ Ext.log.warn('Sync called while a sync operation is in progress. ' + 'Consider configuring autoSync as false.');
+ }
+ me.needsSync = false;
+ if (toCreate.length > 0) {
+ operations.create = toCreate;
+ needsSync = true;
+ }
+ if (toUpdate.length > 0) {
+ operations.update = toUpdate;
+ needsSync = true;
+ }
+ if (toDestroy.length > 0) {
+ operations.destroy = toDestroy;
+ needsSync = true;
+ }
+ if (needsSync && me.fireEvent('beforesync', operations) !== false) {
+ me.isSyncing = true;
+ options = options || {};
+ me.proxy.batch(Ext.apply(options, {
+ operations: operations,
+ listeners: me.getBatchListeners(),
+ $destroyOwner: me
+ }));
+ }
+ return me;
+ },
+ /**
+ * @private
+ * Returns an object which is passed in as the listeners argument to proxy.batch inside
+ * this.sync. This is broken out into a separate function to allow for customisation
+ * of the listeners
+ * @return {Object} The listeners object
+ */
+ getBatchListeners: function() {
+ var me = this,
+ listeners = {
+ scope: me,
+ exception: me.onBatchException,
+ complete: me.onBatchComplete
+ };
+ if (me.batchUpdateMode === 'operation') {
+ listeners.operationcomplete = me.onBatchOperationComplete;
+ }
+ return listeners;
+ },
+ /**
+ * Saves all pending changes via the configured {@link #proxy}. Use {@link #sync} instead.
+ * @deprecated 4.0.0 Will be removed in the next major version
+ */
+ save: function() {
+ return this.sync.apply(this, arguments);
+ },
+ /**
+ * Marks this store as needing a load. When the current executing event handler exits,
+ * this store will send a request to load using its configured {@link #proxy}.
+ *
+ * Upon return of the data from whatever data source the proxy connected to, the retrieved
+ * {@link Ext.data.Model records} will be loaded into this store, and the optional callback
+ * will be called. Example usage:
+ *
+ * store.load({
+ * scope: this,
+ * callback: function(records, operation, success) {
+ * // the operation object
+ * // contains all of the details of the load operation
+ * console.log(records);
+ * }
+ * });
+ *
+ * If the callback scope does not need to be set, a function can simply be passed:
+ *
+ * store.load(function(records, operation, success) {
+ * console.log('loaded records');
+ * });
+ *
+ * @param {Object} [options] This is passed into the
+ * {@link Ext.data.operation.Operation Operation} object that is created and then sent to the
+ * proxy's {@link Ext.data.proxy.Proxy#read} function. In addition to the options listed below,
+ * this object may contain properties to configure the
+ * {@link Ext.data.operation.Operation Operation}.
+ * @param {Function} [options.callback] A function which is called when the response arrives.
+ * @param {Ext.data.Model[]} options.callback.records Array of records.
+ * @param {Ext.data.operation.Operation} options.callback.operation The Operation itself.
+ * @param {Boolean} options.callback.success `true` when operation completed successfully.
+ * @param {Boolean} [options.addRecords=false] Specify as `true` to *add* the incoming records
+ * rather than the default which is to have the incoming records *replace* the existing store
+ * contents.
+ *
+ * @return {Ext.data.Store} this
+ * @since 1.1.0
+ */
+ load: function(options) {
+ var me = this;
+ // Legacy option. Specifying a function was allowed.
+ if (typeof options === 'function') {
+ options = {
+ callback: options
+ };
+ } else {
+ // We may mutate the options object in setLoadOptions.
+ options = options ? Ext.Object.chain(options) : {};
+ }
+ me.pendingLoadOptions = options;
+ // If we are configured to load asynchronously (the default for async proxies)
+ // then schedule a flush, unless one is already scheduled.
+ if (me.getAsynchronousLoad()) {
+ if (!me.loadTimer) {
+ me.loadTimer = Ext.asap(me.flushLoad, me);
+ }
+ } else // If we are configured to load synchronously (the default for sync proxies)
+ // then flush the load now.
+ {
+ me.flushLoad();
+ }
+ return me;
+ },
+ /**
+ * Called when the event handler which called the {@link #method-load} method exits.
+ */
+ flushLoad: function() {
+ var me = this,
+ options = me.pendingLoadOptions,
+ operation;
+ if (me.destroying || me.destroyed) {
+ return;
+ }
+ // If it gets called programatically before the timer fired, the listener will need
+ // cancelling.
+ me.clearLoadTask();
+ if (!options) {
+ return;
+ }
+ me.setLoadOptions(options);
+ if (me.getRemoteSort() && options.sorters) {
+ me.fireEvent('beforesort', me, options.sorters);
+ }
+ operation = Ext.apply({
+ internalScope: me,
+ internalCallback: me.onProxyLoad,
+ scope: me
+ }, options);
+ me.lastOptions = operation;
+ operation = me.createOperation('read', operation);
+ if (me.fireEvent('beforeload', me, operation) !== false) {
+ me.onBeforeLoad(operation);
+ me.loading = true;
+ // Internal event, fired after the flag is set, we need
+ // to fire this beforeload is too early
+ if (me.hasListeners.beginload) {
+ me.fireEvent('beginload', me, operation);
+ }
+ operation.execute();
+ } else {
+ if (me.getAsynchronousLoad()) {
+ operation.abort();
+ }
+ operation.setCompleted();
+ }
+ },
+ /**
+ * Reloads the store using the last options passed to the {@link #method-load} method.
+ * You can use the reload method to reload the store using the parameters from the last load()
+ * call. For example:
+ *
+ * store.load({
+ * params : {
+ * userid : 22216
+ * }
+ * });
+ *
+ * //...
+ *
+ * store.reload();
+ *
+ * The initial {@link #method-load} execution will pass the `userid` parameter in the request.
+ * The {@link #reload} execution will also send the same `userid` parameter in its request
+ * as it will reuse the `params` object from the last {@link #method-load} call.
+ *
+ * You can override a param by passing in the config object with the `params` object:
+ *
+ * store.load({
+ * params : {
+ * userid : 22216,
+ * foo : 'bar'
+ * }
+ * });
+ *
+ * //...
+ *
+ * store.reload({
+ * params : {
+ * userid : 1234
+ * }
+ * });
+ *
+ * The initial {@link #method-load} execution sends the `userid` and `foo` parameters but in the
+ * {@link #reload} it only sends the `userid` paramter because you are overriding the `params`
+ * config not just overriding the one param. To only change a single param but keep other
+ * params, you will have to get the last params from the {@link #lastOptions} property:
+ *
+ * // make a copy of the last params so we don't affect future reload() calls
+ * var lastOptions = store.lastOptions,
+ * lastParams = Ext.clone(lastOptions.params);
+ *
+ * lastParams.userid = 1234;
+ *
+ * store.reload({
+ * params : lastParams
+ * });
+ *
+ * This will now send the `userid` parameter as `1234` and the `foo` param as `'bar'`.
+ *
+ * @param {Object} [options] A config object which contains options which may override the
+ * options passed to the previous load call. See the
+ * {@link #method-load} method for valid configs.
+ */
+ reload: function(options) {
+ return this.load(Ext.apply({}, options, this.lastOptions));
+ },
+ onEndUpdate: function() {
+ var me = this;
+ if (me.needsSync && me.autoSync && !me.autoSyncSuspended) {
+ me.sync();
+ }
+ },
+ /**
+ * @private
+ * A model instance should call this method on the Store it has been
+ * {@link Ext.data.Model#join joined} to.
+ * @param {Ext.data.Model} record The model instance that was edited
+ * @since 3.4.0
+ */
+ afterReject: function(record) {
+ var me = this;
+ // Must pass the 5th param (modifiedFieldNames) as null, otherwise the
+ // event firing machinery appends the listeners "options" object to the arg list
+ // which may get used as the modified fields array by a handler.
+ // This array is used for selective grid cell updating by Grid View.
+ // Null will be treated as though all cells need updating.
+ if (me.contains(record)) {
+ me.onUpdate(record, Ext.data.Model.REJECT, null);
+ me.fireEvent('update', me, record, Ext.data.Model.REJECT, null);
+ me.fireEvent('datachanged', me);
+ }
+ },
+ /**
+ * A model instance should call this method on the Store it has been
+ * {@link Ext.data.Model#join joined} to.
+ * @param {Ext.data.Model} record The model instance that was edited.
+ * @param {String[]} [modifiedFieldNames] (private)
+ * @since 3.4.0
+ * @private
+ */
+ afterCommit: function(record, modifiedFieldNames) {
+ var me = this;
+ if (!modifiedFieldNames) {
+ modifiedFieldNames = null;
+ }
+ if (me.contains(record)) {
+ me.onUpdate(record, Ext.data.Model.COMMIT, modifiedFieldNames);
+ me.fireEvent('update', me, record, Ext.data.Model.COMMIT, modifiedFieldNames);
+ me.fireEvent('datachanged', me);
+ }
+ },
+ afterErase: function(record) {
+ this.onErase(record);
+ },
+ onErase: Ext.emptyFn,
+ onUpdate: Ext.emptyFn,
+ /**
+ * Returns true if the store has a pending load task.
+ * @return {Boolean} `true` if the store has a pending load task.
+ * @private
+ */
+ hasPendingLoad: function() {
+ return !!this.pendingLoadOptions || this.isLoading();
+ },
+ /**
+ * Returns true if the Store is currently performing a load operation
+ * @return {Boolean} `true` if the Store is currently loading
+ */
+ isLoading: function() {
+ return !!this.loading;
+ },
+ /**
+ * Returns `true` if the Store has been loaded.
+ * @return {Boolean} `true` if the Store has been loaded.
+ */
+ isLoaded: function() {
+ return this.loadCount > 0;
+ },
+ /**
+ * Suspends automatically syncing the Store with its Proxy. Only applicable if
+ * {@link #autoSync} is `true`
+ */
+ suspendAutoSync: function() {
+ ++this.autoSyncSuspended;
+ },
+ /**
+ * Resumes automatically syncing the Store with its Proxy. Only applicable if
+ * {@link #autoSync} is `true`
+ * @param {Boolean} syncNow Pass `true` to synchronize now. Only synchronizes with the Proxy
+ * if the suspension count has gone to zero (We are not under a higher level of suspension)
+ *
+ */
+ resumeAutoSync: function(syncNow) {
+ var me = this;
+ if (!me.autoSyncSuspended) {
+ Ext.log.warn('Mismatched call to resumeAutoSync - auto synchronization ' + 'is currently not suspended.');
+ }
+ if (me.autoSyncSuspended && !--me.autoSyncSuspended) {
+ if (syncNow) {
+ me.sync();
+ }
+ }
+ },
+ /**
+ * Removes all records from the store. This method does a "fast remove",
+ * individual remove events are not called. The {@link #clear} event is
+ * fired upon completion.
+ * @method
+ * @since 1.1.0
+ */
+ removeAll: Ext.emptyFn,
+ // individual store subclasses should implement a "fast" remove
+ // and fire a clear event afterwards
+ // to be implemented by subclasses
+ clearData: Ext.emptyFn,
+ privates: {
+ /**
+ * @private
+ * Returns the array of records which have been removed since the last time this store
+ * was synced.
+ *
+ * This is used internally, when purging removed records after a successful sync.
+ * This is overridden by TreeStore because TreeStore accumulates deleted records on removal
+ * of child nodes from their parent, *not* on removal of records from its collection.
+ * The collection has records added on expand, and removed on collapse.
+ */
+ getRawRemovedRecords: function() {
+ return this.removed;
+ },
+ onExtraParamsChanged: function() {},
+ clearLoadTask: function() {
+ this.pendingLoadOptions = this.loadTimer = Ext.unasap(this.loadTimer);
+ },
+ cleanRemoved: function() {
+ // Must use class-specific getRawRemovedRecords.
+ // Regular Stores add to the "removed" property on remove.
+ // TreeStores are having records removed all the time; node collapse removes.
+ // TreeStores add to the "removedNodes" property onNodeRemove
+ var removed = this.getRawRemovedRecords(),
+ len, i;
+ if (removed) {
+ for (i = 0 , len = removed.length; i < len; ++i) {
+ removed[i].unjoin(this);
+ }
+ removed.length = 0;
+ }
+ },
+ createOperation: function(type, options) {
+ var me = this,
+ proxy = me.getProxy(),
+ listeners;
+ if (!me.proxyListeners) {
+ listeners = {
+ scope: me,
+ destroyable: true,
+ beginprocessresponse: me.beginUpdate,
+ endprocessresponse: me.endUpdate
+ };
+ if (!me.disableMetaChangeEvent) {
+ listeners.metachange = me.onMetaChange;
+ }
+ me.proxyListeners = proxy.on(listeners);
+ }
+ return proxy.createOperation(type, options);
+ },
+ createImplicitModel: function(fields) {
+ var me = this,
+ modelCfg = {
+ extend: me.implicitModel,
+ statics: {
+ defaultProxy: 'memory'
+ }
+ },
+ proxy, model;
+ if (fields) {
+ modelCfg.fields = fields;
+ }
+ model = Ext.define(null, modelCfg);
+ me.setModel(model);
+ proxy = me.getProxy();
+ if (proxy) {
+ model.setProxy(proxy);
+ } else {
+ me.setProxy(model.getProxy());
+ }
+ },
+ loadsSynchronously: function() {
+ return this.getProxy().isSynchronous;
+ },
+ onBeforeLoad: Ext.privateFn,
+ removeFromRemoved: function(record) {
+ // Must use class-specific getRawRemovedRecords.
+ // Regular Stores add to the "removed" property on remove.
+ // TreeStores are having records removed all the time; node collapse removes.
+ // TreeStores add to the "removedNodes" property onNodeRemove
+ var removed = this.getRawRemovedRecords();
+ if (removed) {
+ Ext.Array.remove(removed, record);
+ record.unjoin(this);
+ }
+ },
+ setLoadOptions: function(options) {
+ var me = this,
+ filters, sorters;
+ if (me.getRemoteFilter()) {
+ filters = me.getFilters(false);
+ if (filters && filters.getCount()) {
+ options.filters = filters.getRange();
+ }
+ }
+ if (me.getRemoteSort()) {
+ sorters = me.getSorters(false);
+ if (sorters && sorters.getCount()) {
+ options.sorters = sorters.getRange();
+ }
+ }
+ }
+ }
+});
+
+/**
+ * Encapsulates a grouped collection of records within a {@link Ext.util.Collection}
+ */
+Ext.define('Ext.util.Group', {
+ extend: Ext.util.Collection,
+ isGroup: true,
+ config: {
+ groupKey: null
+ },
+ // Group collections must have a higher priority than normal collections. This ensures
+ // that their endupdate handlers for filters and sorters run prior to the endupdate
+ // handler of the store's main collection, and so when the user handles events such
+ // as sort/datachanged, the groups have already been sorted and filtered.
+ $endUpdatePriority: 2001,
+ manageSorters: false
+});
+
+/**
+ * Encapsulates a group of records. Can provide a
+ * {@link #getSummaryRecord} summary record.
+ *
+ * @since 6.5.0
+ */
+Ext.define('Ext.data.Group', {
+ extend: Ext.util.Group,
+ isDataGroup: true,
+ store: null,
+ /**
+ * Returns the summary results for the group.
+ * @return {Ext.data.Model}
+ */
+ getSummaryRecord: function() {
+ var me = this,
+ summaryRecord = me.summaryRecord,
+ store = me.store,
+ generation = store.getData().generation,
+ M, T;
+ if (!summaryRecord) {
+ M = store.getModel();
+ T = M.getSummaryModel();
+ me.summaryRecord = summaryRecord = new T();
+ }
+ if (!summaryRecord.isRemote && summaryRecord.summaryGeneration !== generation) {
+ summaryRecord.calculateSummary(me.items);
+ summaryRecord.summaryGeneration = generation;
+ }
+ return summaryRecord;
+ }
+});
+
+/**
+ * A mixin that provides common store methods for Ext.data.Store & Ext.data.ChainedStore.
+ * @private
+ */
+Ext.define('Ext.data.LocalStore', {
+ extend: Ext.Mixin,
+ mixinConfig: {
+ id: 'localstore',
+ after: {
+ fireGroupChangeEvent: 'onGrouperChange'
+ }
+ },
+ config: {
+ extraKeys: null
+ },
+ applyExtraKeys: function(extraKeys) {
+ var indexName,
+ data = this.getData();
+ // Add the extra keys to the data collection
+ data.setExtraKeys(extraKeys);
+ // Pluck the extra keys out so that we can keep them by index name
+ extraKeys = data.getExtraKeys();
+ for (indexName in extraKeys) {
+ this[indexName] = extraKeys[indexName];
+ }
+ },
+ /**
+ * Adds Model instance to the Store. This method accepts either:
+ *
+ * - An array of Model instances or Model configuration objects.
+ * - Any number of Model instance or Model configuration object arguments.
+ *
+ * The new Model instances will be added at the end of the existing collection.
+ *
+ * Sample usage:
+ *
+ * myStore.add({some: 'data'}, {some: 'other data'});
+ *
+ * Note that if this Store is sorted, the new Model instances will be inserted
+ * at the correct point in the Store to maintain the sort order.
+ *
+ * @param {Ext.data.Model[]/Ext.data.Model.../Object[]/Object...} record An array of
+ * records or configuration objects, or variable number of record or config arguments.
+ * @return {Ext.data.Model[]} The record instances that were added.
+ */
+ add: function(record) {
+ return this.insert(this.getCount(), arguments.length === 1 ? record : arguments);
+ },
+ constructDataCollection: function() {
+ var result = new Ext.util.Collection({
+ id: this.getId() + '-data',
+ rootProperty: 'data',
+ groupConfig: {
+ xclass: 'Ext.data.Group',
+ store: this
+ }
+ });
+ // Add this store as an observer immediately so that we are informed of any
+ // synchronous autoLoad which may occur in this event.
+ result.addObserver(this);
+ return result;
+ },
+ /**
+ * Converts a literal to a model, if it's not a model already
+ * @private
+ * @param {Ext.data.Model/Object} record The record to create
+ * @return {Ext.data.Model}
+ */
+ createModel: function(record) {
+ var session = this.getSession(),
+ Model;
+ if (!record.isModel) {
+ Model = this.getModel();
+ record = new Model(record, session);
+ }
+ return record;
+ },
+ createFiltersCollection: function() {
+ return this.getData().getFilters();
+ },
+ createSortersCollection: function() {
+ var sorters = this.getData().getSorters();
+ sorters.setSorterConfigure(this.addFieldTransform, this);
+ return sorters;
+ },
+ /**
+ * Get the summary record for this store. See {@link Ext.data.Model#summary}.
+ * @return {Ext.data.Model}
+ * @since 6.5.0
+ */
+ getSummaryRecord: function() {
+ var me = this,
+ summaryRecord = me.summaryRecord,
+ data = me.getData(),
+ generation = data.generation,
+ T;
+ if (!summaryRecord) {
+ T = me.getModel().getSummaryModel();
+ me.summaryRecord = summaryRecord = new T();
+ }
+ if (!summaryRecord.isRemote && summaryRecord.summaryGeneration !== generation) {
+ summaryRecord.calculateSummary(data.items);
+ summaryRecord.summaryGeneration = generation;
+ }
+ return summaryRecord;
+ },
+ onCollectionBeginUpdate: function() {
+ this.beginUpdate();
+ },
+ onCollectionEndUpdate: function() {
+ this.endUpdate();
+ },
+ // When the collection informs us that it has sorted, this LocalStore must react.
+ // AbstractStore#onSorterEndUpdate does the correct thing (fires a refresh) if remote sorting
+ // is false
+ onCollectionSort: function() {
+ this.onSorterEndUpdate();
+ },
+ // When the collection informs us that it has filtered, this LocalStore must react.
+ // AbstractStore#onFilterEndUpdate does the correct thing (fires a refresh) if remote sorting
+ // is false
+ onCollectionFilter: function() {
+ this.onFilterEndUpdate();
+ },
+ onGrouperChange: function(grouper) {
+ this.callObservers('GrouperChange', [
+ grouper
+ ]);
+ },
+ notifySorterChange: function() {
+ this.getData().onSorterChange();
+ },
+ forceLocalSort: function() {
+ var sorters = this.getSorters();
+ // Sorter collection must inform all interested parties.
+ // We cannot just tell our data Collection to react - there
+ // may be GroupCollections hooked into the endUpdate call.
+ sorters.beginUpdate();
+ sorters.endUpdate();
+ },
+ // Inherit docs
+ contains: function(record) {
+ return this.indexOf(record) > -1;
+ },
+ /**
+ * Calls the specified function for each {@link Ext.data.Model record} in the store.
+ *
+ * When store is filtered, only loops over the filtered records.
+ *
+ * @param {Function} fn The function to call. The {@link Ext.data.Model Record} is passed
+ * as the first parameter. Returning `false` aborts and exits the iteration.
+ * @param {Object} [scope] The scope (`this` reference) in which the function is executed.
+ * Defaults to the current {@link Ext.data.Model record} in the iteration.
+ * @param {Object/Boolean} [includeOptions] An object which contains options which
+ * modify how the store is traversed. Or simply the `filtered` option.
+ * @param {Boolean} [includeOptions.filtered] Pass `true` to include filtered out
+ * nodes in the iteration.
+ */
+ each: function(fn, scope, includeOptions) {
+ var data = this.getData(),
+ bypassFilters = includeOptions,
+ len, record, i;
+ if (typeof includeOptions === 'object') {
+ bypassFilters = includeOptions.filtered;
+ }
+ if (bypassFilters && data.filtered) {
+ data = data.getSource();
+ }
+ data = data.items.slice(0);
+ // safe for re-entrant calls
+ len = data.length;
+ for (i = 0; i < len; ++i) {
+ record = data[i];
+ if (fn.call(scope || record, record, i, len) === false) {
+ break;
+ }
+ }
+ },
+ /**
+ * Collects unique values for a particular dataIndex from this store.
+ *
+ * Note that the `filtered` option can also be passed as a separate parameter for
+ * compatibility with previous versions.
+ *
+ * var store = Ext.create('Ext.data.Store', {
+ * fields: ['name'],
+ * data: [{
+ * name: 'Larry'
+ * }, {
+ * name: 'Darryl'
+ * }, {
+ * name: 'Darryl'
+ * }]
+ * });
+ *
+ * store.collect('name');
+ * // returns ["Larry", "Darryl"]
+ *
+ * @param {String} property The property to collect
+ * @param {Object} [includeOptions] An object which contains options which modify how
+ * the store is traversed. For compatibility, this argument may be the `allowNull`
+ * value itself. If so, the next argument is the `filtered` value.
+ * @param {Boolean} [includeOptions.allowNull] Pass true to allow null, undefined or
+ * empty string values.
+ * @param {Boolean} [includeOptions.filtered] Pass `true` to collect from all records,
+ * even ones which are filtered.
+ * @param {Boolean} [filtered] This argument only applies when the legacy call form
+ * is used and `includeOptions` is actually the `allowNull` value.
+ *
+ * @return {Object[]} An array of the unique values
+ */
+ collect: function(property, includeOptions, filtered) {
+ var me = this,
+ allowNull = includeOptions,
+ data = me.getData();
+ if (typeof includeOptions === 'object') {
+ filtered = includeOptions.filtered;
+ allowNull = includeOptions.allowNull;
+ }
+ if (filtered && data.filtered) {
+ data = data.getSource();
+ }
+ return data.collect(property, 'data', allowNull);
+ },
+ /**
+ * Get the Record with the specified id.
+ *
+ * This method is not affected by filtering, lookup will be performed from all records
+ * inside the store, filtered or not.
+ *
+ * @param {Mixed} id The id of the Record to find.
+ * @return {Ext.data.Model} The Record with the passed id. Returns null if not found.
+ */
+ getById: function(id) {
+ var data = this.getData();
+ if (data.filtered) {
+ data = data.getSource();
+ }
+ return data.get(id) || null;
+ },
+ /**
+ * @private
+ * Get the Record with the specified internalId.
+ *
+ * This method is not affected by filtering, lookup will be performed from all records
+ * inside the store, filtered or not.
+ *
+ * @param {Mixed} internalId The id of the Record to find.
+ * @return {Ext.data.Model} The Record with the passed internalId. Returns null if not found.
+ */
+ getByInternalId: function(internalId) {
+ var data = this.getData(),
+ keyCfg;
+ if (data.filtered) {
+ if (!data.$hasExtraKeys) {
+ keyCfg = this.makeInternalKeyCfg();
+ data.setExtraKeys(keyCfg);
+ data.$hasExtraKeys = true;
+ }
+ data = data.getSource();
+ }
+ if (!data.$hasExtraKeys) {
+ data.setExtraKeys(keyCfg || this.makeInternalKeyCfg());
+ data.$hasExtraKeys = true;
+ }
+ return data.byInternalId.get(internalId) || null;
+ },
+ /**
+ * Returns the complete unfiltered collection.
+ * @private
+ */
+ getDataSource: function() {
+ var data = this.getData();
+ return data.getSource() || data;
+ },
+ /**
+ * Get the index of the record within the store.
+ *
+ * When store is filtered, records outside of filter will not be found.
+ *
+ * @param {Ext.data.Model} record The Ext.data.Model object to find.
+ * @return {Number} The index of the passed Record. Returns -1 if not found.
+ */
+ indexOf: function(record) {
+ return this.getData().indexOf(record);
+ },
+ /**
+ * Get the index within the store of the Record with the passed id.
+ *
+ * Like #indexOf, this method is affected by filtering.
+ *
+ * @param {String} id The id of the Record to find.
+ * @return {Number} The index of the Record. Returns -1 if not found.
+ */
+ indexOfId: function(id) {
+ return this.indexOf(this.getById(id));
+ },
+ /**
+ * Inserts Model instances into the Store at the given index and fires the add event.
+ * See also {@link #method-add}.
+ *
+ * @param {Number} index The start index at which to insert the passed Records.
+ * @param {Ext.data.Model/Ext.data.Model[]/Object/Object[]} records An `Ext.data.Model`
+ * instance, the data needed to populate an instance or an array of either of these.
+ *
+ * @return {Ext.data.Model[]} records The added records
+ */
+ insert: function(index, records) {
+ var me = this,
+ len, i;
+ if (records) {
+ if (!Ext.isIterable(records)) {
+ records = [
+ records
+ ];
+ } else {
+ records = Ext.Array.clone(records);
+ }
+ len = records.length;
+ }
+ if (!len) {
+ return [];
+ }
+ for (i = 0; i < len; ++i) {
+ records[i] = me.createModel(records[i]);
+ }
+ me.getData().insert(index, records);
+ return records;
+ },
+ /**
+ * Query all the cached records in this Store using a filtering function. The specified function
+ * will be called with each record in this Store. If the function returns `true` the record is
+ * included in the results.
+ *
+ * This method is not affected by filtering, it will always search *all* records in the store
+ * regardless of filtering.
+ *
+ * @param {Function} fn The function to be called. It will be passed the following parameters:
+ * @param {Ext.data.Model} fn.record The record to test for filtering. Access field values
+ * using {@link Ext.data.Model#get}.
+ * @param {Object} fn.id The ID of the Record passed.
+ * @param {Object} [scope] The scope (this reference) in which the function is executed
+ * Defaults to this Store.
+ * @return {Ext.util.Collection} The matched records
+ */
+ queryBy: function(fn, scope) {
+ var data = this.getData();
+ return (data.getSource() || data).createFiltered(fn, scope);
+ },
+ /**
+ * Query all the cached records in this Store by name/value pair.
+ * The parameters will be used to generated a filter function that is given
+ * to the queryBy method.
+ *
+ * This method complements queryBy by generating the query function automatically.
+ *
+ * This method is not affected by filtering, it will always search *all* records in the store
+ * regardless of filtering.
+ *
+ * @param {String} property The property to create the filter function for
+ * @param {String/RegExp} value The string/regex to compare the property value to
+ * @param {Boolean} [anyMatch=false] True to match any part of the string, not just the
+ * beginning.
+ * @param {Boolean} [caseSensitive=false] `true` to create a case-sensitive regex.
+ * @param {Boolean} [exactMatch=false] True to force exact match (^ and $ characters
+ * added to the regex). Ignored if `anyMatch` is `true`.
+ * @return {Ext.util.Collection} The matched records
+ */
+ query: function(property, value, anyMatch, caseSensitive, exactMatch) {
+ var data = this.getData();
+ return (data.getSource() || data).createFiltered(property, value, anyMatch, caseSensitive, exactMatch);
+ },
+ /**
+ * Convenience function for getting the first model instance in the store.
+ *
+ * When store is filtered, will return first item within the filter.
+ *
+ * @param {Boolean} [grouped] True to perform the operation for each group
+ * in the store. The value returned will be an object literal with the key being the group
+ * name and the first record being the value. The grouped parameter is only honored if
+ * the store has a groupField.
+ * @return {Ext.data.Model/undefined} The first model instance in the store, or undefined
+ */
+ first: function(grouped) {
+ return this.getData().first(grouped) || null;
+ },
+ /**
+ * Convenience function for getting the last model instance in the store.
+ *
+ * When store is filtered, will return last item within the filter.
+ *
+ * @param {Boolean} [grouped] True to perform the operation for each group
+ * in the store. The value returned will be an object literal with the key being the group
+ * name and the last record being the value. The grouped parameter is only honored if
+ * the store has a groupField.
+ * @return {Ext.data.Model/undefined} The last model instance in the store, or undefined
+ */
+ last: function(grouped) {
+ return this.getData().last(grouped) || null;
+ },
+ /**
+ * Sums the value of `field` for each {@link Ext.data.Model record} in store
+ * and returns the result.
+ *
+ * When store is filtered, only sums items within the filter.
+ *
+ * @param {String} field A field in each record
+ * @param {Boolean} [grouped] True to perform the operation for each group
+ * in the store. The value returned will be an object literal with the key being the group
+ * name and the sum for that group being the value. The grouped parameter is only honored if
+ * the store has a groupField.
+ * @return {Number} The sum
+ */
+ sum: function(field, grouped) {
+ var data = this.getData();
+ return (grouped && this.isGrouped()) ? data.sumByGroup(field) : data.sum(field);
+ },
+ /**
+ * Gets the count of items in the store.
+ *
+ * When store is filtered, only items within the filter are counted.
+ *
+ * @param {Boolean} [grouped] True to perform the operation for each group
+ * in the store. The value returned will be an object literal with the key being the group
+ * name and the count for each group being the value. The grouped parameter is only honored if
+ * the store has a groupField.
+ * @return {Number} the count
+ */
+ count: function(grouped) {
+ var data = this.getData();
+ return (grouped && this.isGrouped()) ? data.countByGroup() : data.count();
+ },
+ /**
+ * Gets the minimum value in the store.
+ *
+ * When store is filtered, only items within the filter are aggregated.
+ *
+ * @param {String} field The field in each record
+ * @param {Boolean} [grouped] True to perform the operation for each group
+ * in the store. The value returned will be an object literal with the key being the group
+ * name and the minimum in the group being the value. The grouped parameter is only honored if
+ * the store has a groupField.
+ * @return {Object} The minimum value, if no items exist, undefined.
+ */
+ min: function(field, grouped) {
+ var data = this.getData();
+ return (grouped && this.isGrouped()) ? data.minByGroup(field) : data.min(field);
+ },
+ /**
+ * Gets the maximum value in the store.
+ *
+ * When store is filtered, only items within the filter are aggregated.
+ *
+ * @param {String} field The field in each record
+ * @param {Boolean} [grouped] True to perform the operation for each group
+ * in the store. The value returned will be an object literal with the key being the group
+ * name and the maximum in the group being the value. The grouped parameter is only honored if
+ * the store has a groupField.
+ * @return {Object} The maximum value, if no items exist, undefined.
+ */
+ max: function(field, grouped) {
+ var data = this.getData();
+ return (grouped && this.isGrouped()) ? data.maxByGroup(field) : data.max(field);
+ },
+ /**
+ * Gets the average value in the store.
+ *
+ * When store is filtered, only items within the filter are aggregated.
+ *
+ * @param {String} field The field in each record
+ * @param {Boolean} [grouped] True to perform the operation for each group
+ * in the store. The value returned will be an object literal with the key being the group
+ * name and the group average being the value. The grouped parameter is only honored if
+ * the store has a groupField.
+ * @return {Object} The average value, if no items exist, 0.
+ */
+ average: function(field, grouped) {
+ var data = this.getData();
+ return (grouped && this.isGrouped()) ? data.averageByGroup(field) : data.average(field);
+ },
+ /**
+ * Runs the aggregate function for all the records in the store.
+ *
+ * When store is filtered, only items within the filter are aggregated.
+ *
+ * @param {Function} fn The function to execute. The function is called with a single parameter,
+ * an array of records for that group.
+ * @param {Object} [scope] The scope to execute the function in. Defaults to the store.
+ * @param {Boolean} [grouped] True to perform the operation for each group
+ * in the store. The value returned will be an object literal with the key being the group
+ * name and the group average being the value. The grouped parameter is only honored if
+ * the store has a groupField.
+ * @param {String} field The field to get the value from
+ * @return {Object} An object literal with the group names and their appropriate values.
+ */
+ aggregate: function(fn, scope, grouped, field) {
+ var me = this,
+ groups, len, out, group, i;
+ if (grouped && me.isGrouped()) {
+ groups = me.getGroups().items;
+ len = groups.length;
+ out = {};
+ for (i = 0; i < len; ++i) {
+ group = groups[i];
+ out[group.getGroupKey()] = me.getAggregate(fn, scope || me, group.items, field);
+ }
+ return out;
+ } else {
+ return me.getAggregate(fn, scope, me.getData().items, field);
+ }
+ },
+ getAggregate: function(fn, scope, records, field) {
+ var values = [],
+ len = records.length,
+ i;
+ // TODO EXTJSIV-12307 - not the right way to call fn
+ for (i = 0; i < len; ++i) {
+ values[i] = records[i].get(field);
+ }
+ return fn.call(scope || this, records, values);
+ },
+ addObserver: function(observer) {
+ var observers = this.observers;
+ if (!observers) {
+ this.observers = observers = new Ext.util.Collection();
+ }
+ observers.add(observer);
+ },
+ removeObserver: function(observer) {
+ var observers = this.observers;
+ if (observers) {
+ observers.remove(observer);
+ }
+ },
+ callObservers: function(action, args) {
+ var observers = this.observers,
+ len, items, i, methodName, item;
+ if (observers) {
+ items = observers.items;
+ if (args) {
+ args.unshift(this);
+ } else {
+ args = [
+ this
+ ];
+ }
+ for (i = 0 , len = items.length; i < len; ++i) {
+ item = items[i];
+ methodName = 'onSource' + action;
+ if (item[methodName]) {
+ item[methodName].apply(item, args);
+ }
+ }
+ }
+ },
+ /**
+ * Query all the cached records in this Store using a filtering function. The specified function
+ * will be called with each record in this Store. If the function returns `true` the record is
+ * included in the results.
+ *
+ * This method is not affected by filtering, it will always search *all* records in the store
+ * regardless of filtering.
+ *
+ * @param {Function} fn The function to be called. It will be passed the following parameters:
+ * @param {Ext.data.Model} fn.record The record to test for filtering.
+ * @param {Object} [scope] The scope (this reference) in which the function is executed
+ * Defaults to this Store.
+ * @return {Ext.data.Model[]} The matched records.
+ *
+ * @private
+ */
+ queryRecordsBy: function(fn, scope) {
+ var data = this.getData(),
+ matches = [],
+ len, i, record;
+ data = (data.getSource() || data).items;
+ scope = scope || this;
+ for (i = 0 , len = data.length; i < len; ++i) {
+ record = data[i];
+ if (fn.call(scope, record) === true) {
+ matches.push(record);
+ }
+ }
+ return matches;
+ },
+ /**
+ * Query all the cached records in this Store by field.
+ *
+ * This method is not affected by filtering, it will always search *all* records in the store
+ * regardless of filtering.
+ *
+ * @param {String} field The field from each record to use.
+ * @param {Object} value The value to match.
+ * @return {Ext.data.Model[]} The matched records.
+ *
+ * @private
+ */
+ queryRecords: function(field, value) {
+ var data = this.getData(),
+ matches = [],
+ len, i, record;
+ data = (data.getSource() || data).items;
+ for (i = 0 , len = data.length; i < len; ++i) {
+ record = data[i];
+ if (record.get(field) === value) {
+ matches.push(record);
+ }
+ }
+ return matches;
+ },
+ privates: {
+ isLast: function(record) {
+ return record === this.last();
+ },
+ makeInternalKeyCfg: function() {
+ return {
+ byInternalId: {
+ property: 'internalId',
+ rootProperty: ''
+ }
+ };
+ }
+ }
+});
+
+/**
+ * ServerProxy is a superclass of {@link Ext.data.proxy.JsonP JsonPProxy} and
+ * {@link Ext.data.proxy.Ajax AjaxProxy}, and would not usually be used directly.
+ * @protected
+ */
+Ext.define('Ext.data.proxy.Server', {
+ extend: Ext.data.proxy.Proxy,
+ alias: 'proxy.server',
+ alternateClassName: 'Ext.data.ServerProxy',
+ isRemote: true,
+ config: {
+ /**
+ * @cfg {String} url
+ * The URL from which to request the data object.
+ */
+ url: '',
+ /**
+ * @cfg {String} [pageParam="page"]
+ * The name of the 'page' parameter to send in a request. Defaults to 'page'. Set this to
+ * `''` if you don't want to send a page parameter.
+ */
+ pageParam: 'page',
+ /**
+ * @cfg {String} [startParam="start"]
+ * The name of the 'start' parameter to send in a request. Defaults to 'start'. Set this to
+ * `''` if you don't want to send a start parameter.
+ */
+ startParam: 'start',
+ /**
+ * @cfg {String} [limitParam="limit"]
+ * The name of the 'limit' parameter to send in a request. Defaults to 'limit'. Set this to
+ * `''` if you don't want to send a limit parameter.
+ */
+ limitParam: 'limit',
+ /**
+ * @cfg {String} [groupParam="group"]
+ * The name of the 'group' parameter to send in a request. Defaults to 'group'. Set this to
+ * `''` if you don't want to send a group parameter.
+ */
+ groupParam: 'group',
+ /**
+ * @cfg {String} [groupDirectionParam="groupDir"]
+ * The name of the direction parameter to send in a request. **This is only used when
+ * simpleGroupMode is set to true.**
+ * If this is set to the same value as the {@link #groupParam}, then the group property
+ * name *and* direction of each grouper is passed as a single, space separated parameter,
+ * looking like a database `group by` specification.
+ *
+ * So if there are multiple groupers, the single group parameter will look like this:
+ *
+ * ?group=name%20ASC&group=age%20DESC
+ */
+ groupDirectionParam: 'groupDir',
+ /**
+ * @cfg {String} [sortParam="sort"]
+ * The name of the 'sort' parameter to send in a request. Defaults to 'sort'. Set this to
+ * `''` if you don't want to send a sort parameter.
+ */
+ sortParam: 'sort',
+ /**
+ * @cfg {String} [filterParam="filter"]
+ * The name of the 'filter' parameter to send in a request. Defaults to 'filter'. Set this
+ * to `''` if you don't want to send a filter parameter.
+ */
+ filterParam: 'filter',
+ /**
+ * @cfg {String} [directionParam="dir"]
+ * The name of the direction parameter to send in a request. **This is only used when
+ * simpleSortMode is set to true.**
+ *
+ * If this is set to the same value as the {@link #sortParam}, then the sort property name
+ * *and* direction of each sorter is passed as a single, space separated parameter, looking
+ * like a database `order by` specification.
+ *
+ * So if there are multiple sorters, the single sort parameter will look like this:
+ *
+ * ?sort=name%20ASC&sort=age%20DESC
+ */
+ directionParam: 'dir',
+ /**
+ * @cfg {String} [idParam="id"]
+ * The name of the parameter which carries the id of the entity being operated upon.
+ */
+ idParam: 'id',
+ /**
+ * @cfg {Boolean} [simpleSortMode=false]
+ * Enabling simpleSortMode in conjunction with remoteSort will send the sorted field names
+ * in the parameter named by {@link #sortParam}, and the directions for each sorted field
+ * in a parameter named by {@link #directionParam}.
+ *
+ * In the simplest case, with one Sorter, this will result in HTTP parameters like this:
+ *
+ * ?sort=name&dir=ASC
+ *
+ * If there are multiple sorters, the parameters will be encoded like this:
+ *
+ * ?sort=name&sort=age&dir=ASC&dir=DESC
+ */
+ simpleSortMode: false,
+ /**
+ * @cfg {Boolean} [simpleGroupMode=false]
+ * Enabling simpleGroupMode in conjunction with remoteGroup will only send one group
+ * property and a direction when a remote group is requested. The
+ * {@link #groupDirectionParam} and {@link #groupParam} will be sent with the property name
+ * and either 'ASC' or 'DESC'.
+ */
+ simpleGroupMode: false,
+ /**
+ * @cfg {Boolean} [noCache=true]
+ * Disable caching by adding a unique parameter name to the request. Set to false to allow
+ * caching. Defaults to true.
+ */
+ noCache: true,
+ /**
+ * @cfg {String} [cacheString="_dc"]
+ * The name of the cache param added to the url when using noCache. Defaults to "_dc".
+ */
+ cacheString: "_dc",
+ /**
+ * @cfg {Number} timeout
+ * The number of milliseconds to wait for a response. Defaults to 30000 milliseconds
+ * (30 seconds).
+ */
+ timeout: 30000,
+ /**
+ * @cfg {Object} api
+ * Specific urls to call on CRUD action methods "create", "read", "update" and "destroy".
+ * Defaults to:
+ *
+ * api: {
+ * create : undefined,
+ * read : undefined,
+ * update : undefined,
+ * destroy : undefined
+ * }
+ *
+ * The url is built based upon the action being executed [create|read|update|destroy] using
+ * the commensurate {@link #api} property, or if undefined default to the configured
+ * {@link Ext.data.Store}.{@link Ext.data.proxy.Server#url url}.
+ *
+ * For example:
+ *
+ * api: {
+ * create : '/controller/new',
+ * read : '/controller/load',
+ * update : '/controller/update',
+ * destroy : '/controller/destroy_action'
+ * }
+ *
+ * If the specific URL for a given CRUD action is undefined, the CRUD action request will
+ * be directed to the configured {@link Ext.data.proxy.Server#url url}.
+ */
+ api: {
+ create: undefined,
+ read: undefined,
+ update: undefined,
+ destroy: undefined
+ },
+ /**
+ * @cfg {Object} extraParams
+ * Extra parameters that will be included on every request. Individual requests with params
+ * of the same name will override these params when they are in conflict.
+ */
+ extraParams: {}
+ },
+ /**
+ * @event exception
+ * Fires when the server returns an exception. This event may also be listened
+ * to in the event that a request has timed out or has been aborted.
+ * @param {Ext.data.proxy.Proxy} this
+ * @param {Ext.data.Response} response The response that was received
+ * @param {Ext.data.operation.Operation} operation The operation that triggered the request
+ */
+ // in a ServerProxy all four CRUD operations are executed in the same manner, so we delegate to
+ // doRequest in each case
+ create: function() {
+ return this.doRequest.apply(this, arguments);
+ },
+ read: function() {
+ return this.doRequest.apply(this, arguments);
+ },
+ update: function() {
+ return this.doRequest.apply(this, arguments);
+ },
+ erase: function() {
+ return this.doRequest.apply(this, arguments);
+ },
+ /**
+ * Sets a value in the underlying {@link #extraParams}.
+ * @param {String} name The key for the new value
+ * @param {Object} value The value
+ */
+ setExtraParam: function(name, value) {
+ var extraParams = this.getExtraParams();
+ extraParams[name] = value;
+ this.fireEvent('extraparamschanged', extraParams);
+ },
+ updateExtraParams: function(newExtraParams, oldExtraParams) {
+ this.fireEvent('extraparamschanged', newExtraParams);
+ },
+ /**
+ * Creates an {@link Ext.data.Request Request} object from
+ * {@link Ext.data.operation.Operation Operation}.
+ *
+ * This gets called from doRequest methods in subclasses of Server proxy.
+ *
+ * @param {Ext.data.operation.Operation} operation The operation to execute
+ * @return {Ext.data.Request} The request object
+ */
+ buildRequest: function(operation) {
+ var me = this,
+ initialParams = Ext.apply({}, operation.getParams()),
+ // Clone params right now so that they can be mutated at any point further down the
+ // call stack
+ params = Ext.applyIf(initialParams, me.getExtraParams() || {}),
+ request, operationId, idParam;
+ // copy any sorters, filters etc into the params so they can be sent over the wire
+ Ext.applyIf(params, me.getParams(operation));
+ // Set up the entity id parameter according to the configured name.
+ // This defaults to "id". But TreeStore has a "nodeParam" configuration which
+ // specifies the id parameter name of the node being loaded.
+ operationId = operation.getId();
+ idParam = me.getIdParam();
+ if (operationId !== undefined && params[idParam] === undefined) {
+ params[idParam] = operationId;
+ }
+ request = new Ext.data.Request({
+ params: params,
+ action: operation.getAction(),
+ records: operation.getRecords(),
+ url: operation.getUrl(),
+ operation: operation,
+ // this is needed by JsonSimlet in order to properly construct responses for
+ // requests from this proxy
+ proxy: me
+ });
+ request.setUrl(me.buildUrl(request));
+ /*
+ * Save the request on the Operation. Operations don't usually care about Request and
+ * Response data, but in the ServerProxy and any of its subclasses we add both request
+ * and response as they may be useful for further processing
+ */
+ operation.setRequest(request);
+ return request;
+ },
+ /**
+ * Processes response, which may involve updating or committing records, each of which
+ * will inform the owning stores and their interested views. Finally, we may perform
+ * an additional layout if the data shape has changed.
+ *
+ * @protected
+ */
+ processResponse: function(success, operation, request, response) {
+ var me = this,
+ exception, reader, resultSet, meta, destroyOp;
+ // Async callback could have landed at any time, including during and after
+ // destruction. We don't want to unravel the whole response chain in such case.
+ if (me.destroying || me.destroyed) {
+ return;
+ }
+ // Processing a response may involve updating or committing many records
+ // each of which will inform the owning stores, which will ultimately
+ // inform interested views which will most likely have to do a layout
+ // assuming that the data shape has changed.
+ // Bracketing the processing with this event gives owning stores the ability
+ // to fire their own beginupdate/endupdate events which can be used by interested
+ // views to suspend layouts.
+ me.fireEvent('beginprocessresponse', me, response, operation);
+ if (success === true) {
+ reader = me.getReader();
+ if (response.status === 204) {
+ resultSet = reader.getNullResultSet();
+ } else {
+ resultSet = reader.read(me.extractResponseData(response), {
+ // If we're doing an update, we want to construct the models ourselves.
+ recordCreator: operation.getRecordCreator() || reader.defaultRecordCreatorFromServer
+ });
+ }
+ if (!operation.$destroyOwner) {
+ operation.$destroyOwner = me;
+ destroyOp = true;
+ }
+ operation.process(resultSet, request, response);
+ exception = !operation.wasSuccessful();
+ } else {
+ me.setException(operation, response);
+ exception = true;
+ }
+ // It is possible that exception callback destroyed the store and owning proxy,
+ // in which case we can't do nothing except punt.
+ if (me.destroyed) {
+ if (!operation.destroyed && destroyOp && operation.$destroyOwner === me) {
+ operation.destroy();
+ }
+ return;
+ }
+ if (exception) {
+ me.fireEvent('exception', me, response, operation);
+ } else // If a JsonReader detected metadata, process it now.
+ // This will fire the 'metachange' event which the Store processes to fire its own
+ // 'metachange'
+ {
+ meta = resultSet.getMetadata();
+ if (meta) {
+ me.onMetaChange(meta);
+ }
+ }
+ // Ditto
+ if (me.destroyed) {
+ if (!operation.destroyed && destroyOp && operation.$destroyOwner === me) {
+ operation.destroy();
+ }
+ return;
+ }
+ me.afterRequest(request, success);
+ // Tell owning store processing has finished.
+ // It will fire its endupdate event which will cause interested views to
+ // resume layouts.
+ me.fireEvent('endprocessresponse', me, response, operation);
+ if (!operation.destroyed && destroyOp && operation.$destroyOwner === me) {
+ operation.destroy();
+ }
+ },
+ /**
+ * Sets up an exception on the operation
+ * @private
+ * @param {Ext.data.operation.Operation} operation The operation
+ * @param {Object} response The response
+ */
+ setException: function(operation, response) {
+ operation.setException({
+ status: response.status,
+ statusText: response.statusText,
+ response: response
+ });
+ },
+ /**
+ * @method
+ * Template method to allow subclasses to specify how to get the response for the reader.
+ * @template
+ * @private
+ * @param {Object} response The server response
+ * @return {Object} The response data to be used by the reader
+ */
+ extractResponseData: Ext.identityFn,
+ /**
+ * Encode any values being sent to the server. Can be overridden in subclasses.
+ * @protected
+ * @param {Array} value An array of sorters/filters.
+ * @return {Object} The encoded value
+ */
+ applyEncoding: function(value) {
+ return Ext.encode(value);
+ },
+ /**
+ * Encodes the array of {@link Ext.util.Sorter} objects into a string to be sent in the request
+ * url. By default, this simply JSON-encodes the sorter data
+ * @param {Ext.util.Sorter[]} sorters The array of {@link Ext.util.Sorter Sorter} objects
+ * @param {Boolean} [preventArray=false] Prevents the items from being output as an array.
+ * @return {String} The encoded sorters
+ */
+ encodeSorters: function(sorters, preventArray) {
+ var out = [],
+ length = sorters.length,
+ i;
+ for (i = 0; i < length; i++) {
+ out[i] = sorters[i].serialize();
+ }
+ return this.applyEncoding(preventArray ? out[0] : out);
+ },
+ /**
+ * Encodes the array of {@link Ext.util.Filter} objects into a string to be sent in the request
+ * url. By default, this simply JSON-encodes the filter data
+ * @param {Ext.util.Filter[]} filters The array of {@link Ext.util.Filter Filter} objects
+ * @return {String} The encoded filters
+ */
+ encodeFilters: function(filters) {
+ var out = [],
+ length = filters.length,
+ encode, i;
+ for (i = 0; i < length; i++) {
+ encode |= filters[i].serializeTo(out);
+ }
+ // If any Filters return Objects encapsulating their full state, then the parameters
+ // needs JSON encoding.
+ return encode ? this.applyEncoding(out) : out;
+ },
+ /**
+ * @private
+ * Copy any sorters, filters etc into the params so they can be sent over the wire
+ */
+ getParams: function(operation) {
+ if (!operation.isReadOperation) {
+ return {};
+ }
+ /* eslint-disable-next-line vars-on-top */
+ var me = this,
+ params = {},
+ grouper = operation.getGrouper(),
+ sorters = operation.getSorters(),
+ filters = operation.getFilters(),
+ page = operation.getPage(),
+ start = operation.getStart(),
+ limit = operation.getLimit(),
+ simpleSortMode = me.getSimpleSortMode(),
+ simpleGroupMode = me.getSimpleGroupMode(),
+ pageParam = me.getPageParam(),
+ startParam = me.getStartParam(),
+ limitParam = me.getLimitParam(),
+ groupParam = me.getGroupParam(),
+ groupDirectionParam = me.getGroupDirectionParam(),
+ sortParam = me.getSortParam(),
+ filterParam = me.getFilterParam(),
+ directionParam = me.getDirectionParam(),
+ hasGroups, index;
+ if (pageParam && page) {
+ params[pageParam] = page;
+ }
+ if (startParam && (start || start === 0)) {
+ params[startParam] = start;
+ }
+ if (limitParam && limit) {
+ params[limitParam] = limit;
+ }
+ hasGroups = groupParam && grouper;
+ if (hasGroups) {
+ // Grouper is a subclass of sorter, so we can just use the sorter method
+ if (simpleGroupMode) {
+ params[groupParam] = grouper.getProperty();
+ // Allow for direction to be encoded into the same parameter
+ if (groupDirectionParam === groupParam) {
+ params[groupParam] += ' ' + grouper.getDirection();
+ } else {
+ params[groupDirectionParam] = grouper.getDirection();
+ }
+ } else {
+ params[groupParam] = me.encodeSorters([
+ grouper
+ ], true);
+ }
+ }
+ /* eslint-disable max-len */
+ if (sortParam && sorters && sorters.length > 0) {
+ if (simpleSortMode) {
+ // Group will be included in sorters, so skip sorter 0 if groups
+ for (index = (sorters.length > 1 && hasGroups) ? 1 : 0; index < sorters.length; index++) {
+ // Allow for direction to be encoded into the same parameter
+ if (directionParam === sortParam) {
+ params[sortParam] = Ext.Array.push(params[sortParam] || [], sorters[index].getProperty() + ' ' + sorters[index].getDirection());
+ } else {
+ params[sortParam] = Ext.Array.push(params[sortParam] || [], sorters[index].getProperty());
+ params[directionParam] = Ext.Array.push(params[directionParam] || [], sorters[index].getDirection());
+ }
+ }
+ } else {
+ params[sortParam] = me.encodeSorters(sorters);
+ }
+ }
+ /* eslint-enable max-len */
+ if (filterParam && filters && filters.length > 0) {
+ params[filterParam] = me.encodeFilters(filters);
+ }
+ return params;
+ },
+ /**
+ * Generates a url based on a given Ext.data.Request object. By default, ServerProxy's buildUrl
+ * will add the cache-buster param to the end of the url. Subclasses may need to perform
+ * additional modifications to the url.
+ * @param {Ext.data.Request} request The request object
+ * @return {String} The url
+ */
+ buildUrl: function(request) {
+ var me = this,
+ url = me.getUrl(request);
+ if (!url) {
+ Ext.raise("You are using a ServerProxy but have not supplied it with a url.");
+ }
+ if (me.getNoCache()) {
+ url = Ext.urlAppend(url, Ext.String.format("{0}={1}", me.getCacheString(), Ext.Date.now()));
+ }
+ return url;
+ },
+ /**
+ * Get the url for the request taking into account the order of priority,
+ * - The request
+ * - The api
+ * - The url
+ * @private
+ * @param {Ext.data.Request} request The request
+ * @return {String} The url
+ */
+ getUrl: function(request) {
+ var url;
+ if (request) {
+ url = request.getUrl() || this.getApi()[request.getAction()];
+ }
+ return url ? url : this.callParent();
+ },
+ /**
+ * In ServerProxy subclasses, the {@link #method-create}, {@link #method-read},
+ * {@link #method-update}, and {@link #method-erase} methods all pass through to doRequest.
+ * Each ServerProxy subclass must implement the doRequest method - see
+ * {@link Ext.data.proxy.JsonP} and {@link Ext.data.proxy.Ajax} for examples. This method
+ * carries the same signature as each of the methods that delegate to it.
+ *
+ * @param {Ext.data.operation.Operation} operation The Ext.data.operation.Operation object
+ * @param {Function} callback The callback function to call when the Operation has completed
+ * @param {Object} scope The scope in which to execute the callback
+ */
+ doRequest: function(operation, callback, scope) {
+ Ext.raise("The doRequest function has not been implemented on your " + "Ext.data.proxy.Server subclass. See src/data/ServerProxy.js for details");
+ },
+ /**
+ * Optional callback function which can be used to clean up after a request has been completed.
+ * @param {Ext.data.Request} request The Request object
+ * @param {Boolean} success True if the request was successful
+ * @protected
+ * @template
+ * @method
+ */
+ afterRequest: Ext.emptyFn,
+ destroy: function() {
+ var me = this;
+ me.destroying = true;
+ // Don't force Reader and Writer creation if they weren't yet instantiated
+ me.reader = me.writer = Ext.destroy(me.reader, me.writer);
+ me.callParent();
+ // This just makes it hard to ask "was destroy() called?":
+ // me.destroying = false; // removed in 7.0
+ me.destroyed = true;
+ }
+});
+
+/**
+ * AjaxProxy is one of the most widely-used ways of getting data into your application. It uses AJAX
+ * requests to load data from the server, usually to be placed into a {@link Ext.data.Store Store}.
+ * Let's take a look at a typical setup. Here we're going to set up a Store that has an Ajax Proxy.
+ * To prepare, we'll also set up a {@link Ext.data.Model Model}:
+ *
+ * Ext.define('User', {
+ * extend: 'Ext.data.Model',
+ * fields: ['id', 'name', 'email']
+ * });
+ *
+ * // The Store contains the AjaxProxy as an inline configuration
+ * var store = Ext.create('Ext.data.Store', {
+ * model: 'User',
+ * proxy: {
+ * type: 'ajax',
+ * url: 'users.json'
+ * }
+ * });
+ *
+ * store.load();
+ *
+ * Our example is going to load user data into a Store, so we start off by defining a
+ * {@link Ext.data.Model Model} with the fields that we expect the server to return. Next we set up
+ * the Store itself, along with a {@link Ext.data.Store#proxy proxy} configuration.
+ * This configuration was automatically turned into an Ext.data.proxy.Ajax instance, with the url
+ * we specified being passed into AjaxProxy's constructor. It's as if we'd done this:
+ *
+ * new Ext.data.proxy.Ajax({
+ * url: 'users.json',
+ * model: 'User',
+ * reader: 'json'
+ * });
+ *
+ * A couple of extra configurations appeared here - {@link #model} and {@link #reader}. These are
+ * set by default when we create the proxy via the Store - the Store already knows about the Model,
+ * and Proxy's default {@link Ext.data.reader.Reader Reader} is
+ * {@link Ext.data.reader.Json JsonReader}.
+ *
+ * Now when we call store.load(), the AjaxProxy springs into action, making a request to the url
+ * we configured ('users.json' in this case). As we're performing a read, it sends a GET request
+ * to that url (see {@link #actionMethods} to customize this - by default any kind of read will be
+ * sent as a GET request and any kind of write will be sent as a POST request).
+ *
+ * # Limitations
+ *
+ * AjaxProxy cannot be used to retrieve data from other domains. If your application is running
+ * on http://domainA.com it cannot load data from http://domainB.com because browsers have
+ * a built-in security policy that prohibits domains talking to each other via AJAX.
+ *
+ * If you need to read data from another domain and can't set up a proxy server (some software
+ * that runs on your own domain's web server and transparently forwards requests to
+ * http://domainB.com, making it look like they actually came from http://domainA.com), you can use
+ * {@link Ext.data.proxy.JsonP} and a technique known as JSON-P (JSON with Padding), which can help
+ * you get around the problem so long as the server on http://domainB.com is set up to support
+ * JSON-P responses. See {@link Ext.data.proxy.JsonP JsonPProxy}'s introduction docs for more
+ * details.
+ *
+ * # Readers and Writers
+ *
+ * AjaxProxy can be configured to use any type of {@link Ext.data.reader.Reader Reader} to decode
+ * the server's response. If no Reader is supplied, AjaxProxy will default to using a
+ * {@link Ext.data.reader.Json JsonReader}. Reader configuration can be passed in as a simple
+ * object, which the Proxy automatically turns into a {@link Ext.data.reader.Reader Reader}
+ * instance:
+ *
+ * var proxy = new Ext.data.proxy.Ajax({
+ * model: 'User',
+ * reader: {
+ * type: 'xml',
+ * rootProperty: 'users'
+ * }
+ * });
+ *
+ * proxy.getReader(); // returns an XmlReader instance based on the config we supplied
+ *
+ * # Url generation
+ *
+ * AjaxProxy automatically inserts any sorting, filtering, paging and grouping options into the url
+ * it generates for each request. These are controlled with the following configuration options:
+ *
+ * - {@link #pageParam} - controls how the page number is sent to the server (see also
+ * {@link #startParam} and {@link #limitParam})
+ * - {@link #sortParam} - controls how sort information is sent to the server
+ * - {@link #groupParam} - controls how grouping information is sent to the server
+ * - {@link #filterParam} - controls how filter information is sent to the server
+ *
+ * Each request sent by AjaxProxy is described by an {@link Ext.data.operation.Operation Operation}.
+ * To see how we can customize the generated urls, let's say we're loading the Proxy
+ * with the following Operation:
+ *
+ * var proxy = new Ext.data.proxy.Ajax({
+ * url: '/users'
+ * });
+ *
+ * var operation = proxy.createOperation('read', {
+ * page: 2
+ * });
+ *
+ * Now we'll issue the request for this Operation by calling {@link #read}:
+ *
+ * proxy.read(operation); // GET /users?page=2
+ *
+ * Easy enough - the Proxy just copied the page property from the Operation. We can customize
+ * how this page data is sent to the server:
+ *
+ * var proxy = new Ext.data.proxy.Ajax({
+ * url: '/users',
+ * pageParam: 'pageNumber'
+ * });
+ *
+ * proxy.read(operation); // GET /users?pageNumber=2
+ *
+ * Alternatively, our Operation could have been configured to send start and limit parameters
+ * instead of page:
+ *
+ * var proxy = new Ext.data.proxy.Ajax({
+ * url: '/users'
+ * });
+ *
+ * var operation = proxy.createOperation('read', {
+ * start: 50,
+ * limit: 25
+ * });
+ *
+ * proxy.read(operation); // GET /users?start=50&limit;=25
+ *
+ * Again we can customize this url:
+ *
+ * var proxy = new Ext.data.proxy.Ajax({
+ * url: '/users',
+ * startParam: 'startIndex',
+ * limitParam: 'limitIndex'
+ * });
+ *
+ * proxy.read(operation); // GET /users?startIndex=50&limitIndex;=25
+ *
+ * AjaxProxy will also send sort and filter information to the server. Let's take a look at how
+ * this looks with a more expressive Operation object:
+ *
+ * var operation = proxy.createOperation('read', {
+ * sorters: [
+ * new Ext.util.Sorter({
+ * property: 'name',
+ * direction: 'ASC'
+ * }),
+ * new Ext.util.Sorter({
+ * property: 'age',
+ * direction: 'DESC'
+ * })
+ * ],
+ * filters: [
+ * new Ext.util.Filter({
+ * property: 'eyeColor',
+ * value: 'brown'
+ * })
+ * ]
+ * });
+ *
+ * This is the type of object that is generated internally when loading a
+ * {@link Ext.data.Store Store} with sorters and filters defined. By default the AjaxProxy will
+ * JSON encode the sorters and filters, resulting in something like this (note that the url
+ * is escaped before sending the request, but is left unescaped here for clarity):
+ *
+ * var proxy = new Ext.data.proxy.Ajax({
+ * url: '/users'
+ * });
+ *
+ * // GET /users?sort=[{"property":"name","direction":"ASC"},
+ * {"property":"age","direction":"DESC"}]
+ * &filter;=[{"property":"eyeColor","value":"brown"}]
+ * proxy.read(operation);
+ *
+ * We can again customize how this is created by supplying a few configuration options. Let's say
+ * our server is set up to receive sorting information is a format like "sortBy=name#ASC,age#DESC".
+ * We can configure AjaxProxy to provide that format like this:
+ *
+ * var proxy = new Ext.data.proxy.Ajax({
+ * url: '/users',
+ * sortParam: 'sortBy',
+ * filterParam: 'filterBy',
+ *
+ * // our custom implementation of sorter encoding -
+ * // turns our sorters into "name#ASC,age#DESC"
+ * encodeSorters: function(sorters) {
+ * var length = sorters.length,
+ * sortStrs = [],
+ * sorter, i;
+ *
+ * for (i = 0; i < length; i++) {
+ * sorter = sorters[i];
+ *
+ * sortStrs[i] = sorter.property + '#' + sorter.direction
+ * }
+ *
+ * return sortStrs.join(",");
+ * }
+ * });
+ *
+ * // GET /users?sortBy=name#ASC,age#DESC&filterBy;=[{"property":"eyeColor","value":"brown"}]
+ * proxy.read(operation);
+ *
+ * We can also provide a custom {@link #encodeFilters} function to encode our filters.
+ *
+ * # Debugging your Ajax Proxy
+ *
+ * If the data is not being loaded into the store as expected, it could be due to a mismatch
+ * between the the way that the {@link #reader} is configured, and the shape of the incoming data.
+ *
+ * To debug from the point that your data arrives back from the network, set a breakpoint inside
+ * the callback function created in the `createRequestCallback` method of the Ajax Proxy class,
+ * and follow the data to where the {@link #reader} attempts to consume it.
+ *
+ * @constructor
+ * Note that if this HttpProxy is being used by a {@link Ext.data.Store Store}, then the Store's
+ * call to {@link Ext.data.Store#method-load load} will override any specified callback and params
+ * options. In this case, use the {@link Ext.data.Store Store}'s events to modify parameters,
+ * or react to loading events.
+ *
+ * @param {Object} config (optional) Config object.
+ * If an options parameter is passed, the singleton {@link Ext.Ajax} object will be used to make
+ * the request.
+ */
+Ext.define('Ext.data.proxy.Ajax', {
+ extend: Ext.data.proxy.Server,
+ alias: 'proxy.ajax',
+ alternateClassName: [
+ 'Ext.data.HttpProxy',
+ 'Ext.data.AjaxProxy'
+ ],
+ isAjaxProxy: true,
+ // Keep a default copy of the action methods here. Ideally could just null
+ // out actionMethods and just check if it exists & has a property, otherwise
+ // fallback to the default. But at the moment it's defined as a public property,
+ // so we need to be able to maintain the ability to modify/access it.
+ defaultActionMethods: {
+ create: 'POST',
+ read: 'GET',
+ update: 'POST',
+ destroy: 'POST'
+ },
+ config: {
+ /**
+ * @cfg {Boolean} binary
+ * True to request binary data from the server. This feature requires
+ * the use of a binary reader such as {@link Ext.data.amf.Reader AMF Reader}
+ */
+ binary: false,
+ /**
+ * @cfg {Object} [headers]
+ * Any headers to add to the Ajax request.
+ *
+ * example:
+ *
+ * proxy: {
+ * headers: {'Content-Type': "text/plain" }
+ * ...
+ * }
+ */
+ headers: undefined,
+ /**
+ * @cfg {Boolean} paramsAsJson
+ * Set to `true` to have any request parameters sent as
+ * {@link Ext.data.Connection#method-request jsonData} where they can be parsed from the
+ * raw request. By default, parameters are sent via the
+ * {@link Ext.data.Connection#method-request params} property.
+ * **Note**: This setting does not apply when the request is sent as a 'GET' request.
+ * See {@link #cfg!actionMethods} for controlling the HTTP verb that is used when sending
+ * requests.
+ */
+ paramsAsJson: false,
+ /**
+ * @cfg {Boolean} withCredentials
+ * This configuration is sometimes necessary when using cross-origin resource sharing.
+ * @accessor
+ */
+ withCredentials: false,
+ /**
+ * @cfg {Boolean} useDefaultXhrHeader
+ * Set this to false to not send the default Xhr header (X-Requested-With) with every
+ * request. This should be set to false when making CORS (cross-domain) requests.
+ * @accessor
+ */
+ useDefaultXhrHeader: true,
+ /**
+ * @cfg {String} username
+ * Most oData feeds require basic HTTP authentication. This configuration allows
+ * you to specify the username.
+ * @accessor
+ */
+ username: null,
+ /**
+ * @cfg {String} password
+ * Most oData feeds require basic HTTP authentication. This configuration allows
+ * you to specify the password.
+ * @accessor
+ */
+ password: null,
+ /**
+ * @cfg {Object} actionMethods
+ * Mapping of action name to HTTP request method. In the basic AjaxProxy these are set to
+ * 'GET' for 'read' actions and 'POST' for 'create', 'update' and 'destroy' actions. The
+ * {@link Ext.data.proxy.Rest} maps these to the correct RESTful methods.
+ */
+ actionMethods: {
+ create: 'POST',
+ read: 'GET',
+ update: 'POST',
+ destroy: 'POST'
+ }
+ },
+ doRequest: function(operation) {
+ var me = this,
+ writer = me.getWriter(),
+ request = me.buildRequest(operation),
+ method = me.getMethod(request),
+ jsonData, params;
+ if (writer && operation.allowWrite()) {
+ request = writer.write(request);
+ }
+ request.setConfig({
+ binary: me.getBinary(),
+ headers: me.getHeaders(),
+ timeout: me.getTimeout(),
+ scope: me,
+ callback: me.createRequestCallback(request, operation),
+ method: method,
+ useDefaultXhrHeader: me.getUseDefaultXhrHeader(),
+ disableCaching: false
+ });
+ // explicitly set it to false, ServerProxy handles caching
+ if (me.responseType != null && Ext.supports.XHR2) {
+ request.setResponseType(me.responseType);
+ }
+ if (method.toUpperCase() !== 'GET' && me.getParamsAsJson()) {
+ params = request.getParams();
+ if (params) {
+ jsonData = request.getJsonData();
+ if (jsonData) {
+ jsonData = Ext.Object.merge({}, jsonData, params);
+ } else {
+ jsonData = params;
+ }
+ request.setJsonData(jsonData);
+ request.setParams(undefined);
+ }
+ }
+ if (me.getWithCredentials()) {
+ request.setWithCredentials(true);
+ request.setUsername(me.getUsername());
+ request.setPassword(me.getPassword());
+ }
+ return me.sendRequest(request);
+ },
+ /**
+ * Fires a request
+ * @param {Ext.data.Request} request The request
+ * @return {Ext.data.Request} The request
+ * @private
+ */
+ sendRequest: function(request) {
+ request.setRawRequest(Ext.Ajax.request(request.getCurrentConfig()));
+ this.lastRequest = request;
+ return request;
+ },
+ /**
+ * Aborts a running request.
+ * @param {Ext.data.Request} [request] The request to abort. If not passed, the most recent
+ * active request will be aborted.
+ */
+ abort: function(request) {
+ request = request || this.lastRequest;
+ if (request) {
+ Ext.Ajax.abort(request.getRawRequest());
+ }
+ },
+ /**
+ * Returns the HTTP method name for a given request. By default this returns based on a lookup
+ * on {@link #cfg!actionMethods}.
+ * @param {Ext.data.Request} request The request object
+ * @return {String} The HTTP method to use (should be one of 'GET', 'POST', 'PUT' or 'DELETE')
+ */
+ getMethod: function(request) {
+ var actions = this.getActionMethods(),
+ action = request.getAction(),
+ method;
+ if (actions) {
+ method = actions[action];
+ }
+ return method || this.defaultActionMethods[action];
+ },
+ /**
+ * @private
+ * TODO: This is currently identical to the JsonPProxy version except for the return function's
+ * signature. There is a lot of code duplication inside the returned function so we need to
+ * find a way to DRY this up.
+ * @param {Ext.data.Request} request The Request object
+ * @param {Ext.data.operation.Operation} operation The Operation being executed
+ * @return {Function} The callback function
+ */
+ createRequestCallback: function(request, operation) {
+ return function(options, success, response) {
+ var me = this;
+ if (request === me.lastRequest) {
+ me.lastRequest = null;
+ }
+ if (!me.destroying && !me.destroyed) {
+ me.processResponse(success, operation, request, response);
+ }
+ };
+ },
+ destroy: function() {
+ this.lastRequest = null;
+ this.callParent();
+ }
+});
+
+/**
+ * The JSON Reader is used by a Proxy to read a server response that is sent back in JSON format.
+ * This usually happens as a result of loading a Store - for example we might create something
+ * like this:
+ *
+ * Ext.define('User', {
+ * extend: 'Ext.data.Model',
+ * fields: ['id', 'name', 'email']
+ * });
+ *
+ * var store = Ext.create('Ext.data.Store', {
+ * model: 'User',
+ * proxy: {
+ * type: 'ajax',
+ * url: 'users.json',
+ * reader: {
+ * type: 'json'
+ * }
+ * }
+ * });
+ *
+ * The example above creates a 'User' model. Models are explained in the
+ * {@link Ext.data.Model Model} docs if you're not already familiar with them.
+ *
+ * We created the simplest type of JSON Reader possible by simply telling our
+ * {@link Ext.data.Store Store}'s {@link Ext.data.proxy.Proxy Proxy} that we want a JSON Reader.
+ * The Store automatically passes the configured model to the Store, so it is as if we passed
+ * this instead:
+ *
+ * reader: {
+ * type: 'json',
+ * model: 'User'
+ * }
+ *
+ * The reader we set up is ready to read data from our server - at the moment it will accept
+ * a response like this:
+ *
+ * [
+ * {
+ * "id": 1,
+ * "name": "Ed Spencer",
+ * "email": "ed@sencha.com"
+ * },
+ * {
+ * "id": 2,
+ * "name": "Abe Elias",
+ * "email": "abe@sencha.com"
+ * }
+ * ]
+ *
+ * ## Reading other JSON formats
+ *
+ * If you already have your JSON format defined and it doesn't look quite like what we have above,
+ * you can usually pass JsonReader a couple of configuration options to make it parse your format.
+ * For example, we can use the {@link #cfg-rootProperty} configuration to parse data that comes back
+ * like this:
+ *
+ * {
+ * "users": [
+ * {
+ * "id": 1,
+ * "name": "Ed Spencer",
+ * "email": "ed@sencha.com"
+ * },
+ * {
+ * "id": 2,
+ * "name": "Abe Elias",
+ * "email": "abe@sencha.com"
+ * }
+ * ]
+ * }
+ *
+ * To parse this we just pass in a {@link #rootProperty} configuration that matches the 'users'
+ * above:
+ *
+ * reader: {
+ * type: 'json',
+ * rootProperty: 'users'
+ * }
+ *
+ * Sometimes the JSON structure is even more complicated. Document databases like CouchDB often
+ * provide metadata around each record inside a nested structure like this:
+ *
+ * {
+ * "total": 122,
+ * "offset": 0,
+ * "users": [
+ * {
+ * "id": "ed-spencer-1",
+ * "value": 1,
+ * "user": {
+ * "id": 1,
+ * "name": "Ed Spencer",
+ * "email": "ed@sencha.com"
+ * }
+ * }
+ * ]
+ * }
+ *
+ * In the case above the record data is nested an additional level inside the "users" array as each
+ * "user" item has additional metadata surrounding it ('id' and 'value' in this case). To parse
+ * data out of each "user" item in the JSON above we need to specify the {@link #record}
+ * configuration like this:
+ *
+ * reader: {
+ * type: 'json',
+ * rootProperty: 'users',
+ * record: 'user'
+ * }
+ *
+ * ## Response MetaData
+ *
+ * The server can return metadata in its response, in addition to the record data, that describe
+ * attributes of the data set itself or are used to reconfigure the Reader. To pass metadata
+ * in the response you simply add a `metaData` attribute to the root of the response data.
+ * The metaData attribute can contain anything, but supports a specific set of properties
+ * that are handled by the Reader if they are present:
+ *
+ * - {@link #rootProperty}: the property name of the root response node containing the record data
+ * - {@link #totalProperty}: property name for the total number of records in the data
+ * - {@link #successProperty}: property name for the success status of the response
+ * - {@link #messageProperty}: property name for an optional response message
+ * - {@link Ext.data.Model#cfg-fields fields}: Config used to reconfigure the Model's fields
+ * before converting the response data into records
+ *
+ * An initial Reader configuration containing all of these properties might look like this
+ * ("fields" would be included in the Model definition, not shown):
+ *
+ * reader: {
+ * type: 'json',
+ * rootProperty: 'root',
+ * totalProperty: 'total',
+ * successProperty: 'success',
+ * messageProperty: 'message'
+ * }
+ *
+ * If you were to pass a response object containing attributes different from those initially
+ * defined above, you could use the `metaData` attribute to reconfigure the Reader on the fly.
+ * For example:
+ *
+ * {
+ * "count": 1,
+ * "ok": true,
+ * "msg": "Users found",
+ * "users": [{
+ * "userId": 123,
+ * "name": "Ed Spencer",
+ * "email": "ed@sencha.com"
+ * }],
+ * "metaData": {
+ * "rootProperty": "users",
+ * "totalProperty": 'count',
+ * "successProperty": 'ok',
+ * "messageProperty": 'msg'
+ * }
+ * }
+ *
+ * You can also place any other arbitrary data you need into the `metaData` attribute which will be
+ * ignored by the Reader, but will be accessible via the Reader's {@link #metaData} property
+ * (which is also passed to listeners via the Proxy's
+ * {@link Ext.data.proxy.Proxy#metachange metachange} event (also relayed by the store).
+ * Application code can then process the passed metadata in any way it chooses.
+ *
+ * A simple example for how this can be used would be customizing the fields for a Model that is
+ * bound to a grid. By passing the `fields` property the Model will be automatically updated by the
+ * Reader internally, but that change will not be reflected automatically in the grid unless you
+ * also update the column configuration. You could do this manually, or you could simply pass
+ * a standard grid {@link Ext.panel.Table#columns column} config object as part of the `metaData`
+ * attribute and then pass that along to the grid. Here's a very simple example for how that
+ * could be accomplished:
+ *
+ * // response format:
+ * {
+ * ...
+ * "metaData": {
+ * "fields": [
+ * { "name": "userId", "type": "int" },
+ * { "name": "name", "type": "string" },
+ * { "name": "birthday", "type": "date", "dateFormat": "Y-j-m" },
+ * ],
+ * "columns": [
+ * { "text": "User ID", "dataIndex": "userId", "width": 40 },
+ * { "text": "User Name", "dataIndex": "name", "flex": 1 },
+ * { "text": "Birthday", "dataIndex": "birthday", "flex": 1,
+ * "format": 'Y-j-m', "xtype": "datecolumn" }
+ * ]
+ * }
+ * }
+ *
+ * The Reader will automatically read the meta fields config and rebuild the Model based on the new
+ * fields, but to handle the new column configuration you would need to handle the metadata within
+ * the application code. This is done simply enough by handling the metachange event on either
+ * the store or the proxy, e.g.:
+ *
+ * var store = Ext.create('Ext.data.Store', {
+ * ...
+ * listeners: {
+ * 'metachange': function(store, meta) {
+ * myGrid.reconfigure(store, meta.columns);
+ * }
+ * }
+ * });
+ *
+ */
+Ext.define('Ext.data.reader.Json', {
+ extend: Ext.data.reader.Reader,
+ alternateClassName: 'Ext.data.JsonReader',
+ alias: 'reader.json',
+ config: {
+ /**
+ * @cfg {String} record The optional location within the JSON response that the record data
+ * itself can be found at. See the JsonReader intro docs for more details. This is not often
+ * needed.
+ */
+ record: null,
+ /**
+ * @cfg {String} [metaProperty]
+ * Name of the property from which to retrieve the `metaData` attribute. See
+ * {@link #metaData}.
+ */
+ metaProperty: 'metaData',
+ /**
+ * @cfg {Boolean} useSimpleAccessors True to ensure that field names/mappings are treated
+ * as literals when reading values.
+ *
+ * For example, by default, using the mapping "foo.bar.baz" will try and read a property foo
+ * from the root, then a property bar from foo, then a property baz from bar. Setting the
+ * simple accessors to true will read the property with the name "foo.bar.baz" direct from
+ * the root object.
+ */
+ useSimpleAccessors: false,
+ /**
+ * @cfg {Boolean} preserveRawData
+ * The reader will keep a copy of the most recent request in the {@link #rawData} property.
+ * For performance reasons, the data object for each record is used directly as the model
+ * data. This means that these objects may be modified and thus modify the raw data.
+ * To ensure the objects are copied, set this option to `true`.
+ * NB: This only applies to items that are read as part of the data array, any other
+ * metadata will not be modified:
+ *
+ * {
+ * "someOtherData": 1, // Won't be modified
+ * "root": [{}, {}, {}] // The objects here will be modified
+ * }
+ */
+ preserveRawData: false
+ },
+ /**
+ * @private
+ */
+ responseType: 'json',
+ updateRootProperty: function() {
+ this.forceBuildExtractors();
+ },
+ updateMetaProperty: function() {
+ this.forceBuildExtractors();
+ },
+ /**
+ * @method readRecords
+ * Reads a JSON object and returns a ResultSet. Uses the internal getTotal and getSuccess
+ * extractors to retrieve meta data from the response, and extractData to turn the JSON data
+ * into model instances.
+ * @param {Object} data The raw JSON data
+ * @param {Object} [readOptions] See {@link #read} for details.
+ * @return {Ext.data.ResultSet} A ResultSet containing model instances and meta data about
+ * the results
+ */
+ getResponseData: function(response) {
+ var error;
+ if (typeof response.responseJson === 'object') {
+ return response.responseJson;
+ }
+ try {
+ return Ext.decode(response.responseText);
+ } catch (ex) {
+ error = this.createReadError(ex.message);
+ Ext.Logger.warn('Unable to parse the JSON returned by the server');
+ this.fireEvent('exception', this, response, error);
+ return error;
+ }
+ },
+ buildExtractors: function(force) {
+ var me = this,
+ emptyFn = Ext.emptyFn;
+ // Will only return true if we need to build
+ if (me.callParent([
+ force
+ ])) {
+ me.getRoot = me.setupExtractor(me.getRootProperty(), Ext.identityFn);
+ me.getGroupRoot = me.setupExtractor(me.getGroupRootProperty(), emptyFn);
+ me.getSummaryRoot = me.setupExtractor(me.getSummaryRootProperty(), emptyFn);
+ me.getMeta = me.setupExtractor(me.getMetaProperty(), emptyFn);
+ }
+ },
+ /**
+ * @private
+ * We're just preparing the data for the superclass by pulling out the record objects we want.
+ * If a {@link #record} was specified we have to pull those out of the larger JSON object,
+ * which is most of what this function is doing
+ * @param {Object} root The JSON root node
+ * @param {Object} [readOptions] See {@link #read} for details.
+ * @return {Ext.data.Model[]} The records
+ */
+ extractData: function(root, readOptions) {
+ var recordName = this.getRecord(),
+ data = [],
+ length, i;
+ if (recordName) {
+ length = root.length;
+ if (!length && Ext.isObject(root)) {
+ length = 1;
+ root = [
+ root
+ ];
+ }
+ for (i = 0; i < length; i++) {
+ data[i] = root[i][recordName];
+ }
+ } else {
+ data = root;
+ }
+ return this.callParent([
+ data,
+ readOptions
+ ]);
+ },
+ getModelData: function(raw) {
+ return this.getPreserveRawData() ? Ext.apply({}, raw) : raw;
+ },
+ /**
+ * @private
+ * @method
+ * Returns an accessor function for the given property string. Gives support for properties
+ * such as the following:
+ *
+ * - 'someProperty'
+ * - 'some.property'
+ * - '["someProperty"]'
+ * - 'values[0]'
+ *
+ * This is used by {@link #buildExtractors} to create optimized extractor functions for
+ * properties that are looked up directly on the source object (e.g. {@link #successProperty},
+ * {@link #messageProperty}, etc.).
+ */
+ createAccessor: (function() {
+ var re = /[\[\.]/;
+ // eslint-disable-line no-useless-escape
+ return function(expr) {
+ var simple = this.getUseSimpleAccessors(),
+ operatorIndex, result, current, parts, part, inExpr, isDot, isLeft, isRight, special, c, i, bracketed, len;
+ if (!(expr || expr === 0)) {
+ return;
+ }
+ if (typeof expr === 'function') {
+ return expr;
+ }
+ if (!simple) {
+ operatorIndex = String(expr).search(re);
+ }
+ if (simple === true || operatorIndex < 0) {
+ result = function(raw) {
+ return raw == null ? null : raw[expr];
+ };
+ } else {
+ // The purpose of this part is to generate a "safe" accessor for any complex
+ // json expression. For example 'foo.bar.baz' will get transformed:
+ // raw.foo && raw.foo.bar && raw.foo.bar.baz
+ current = 'raw';
+ parts = [];
+ part = '';
+ inExpr = 0;
+ len = expr.length;
+ // The <= is intentional here. We handle the last character
+ // being undefined so that we can append any final values at
+ // the end
+ for (i = 0; i <= len; ++i) {
+ c = expr[i];
+ isDot = c === '.';
+ isLeft = c === '[';
+ isRight = c === ']';
+ special = isDot || isLeft || isRight || !c;
+ // If either:
+ // a) Not a special char
+ // b) We're nested more than 1 deep, no single char can bring us out
+ // c) We are in an expr & it's not an ending brace
+ // Then just push the character on
+ if (!special || inExpr > 1 || (inExpr && !isRight)) {
+ part += c;
+ } else if (special) {
+ bracketed = false;
+ if (isLeft) {
+ ++inExpr;
+ } else if (isRight) {
+ --inExpr;
+ bracketed = true;
+ }
+ if (part) {
+ if (bracketed) {
+ part = '[' + part + ']';
+ } else {
+ part = '.' + part;
+ }
+ current += part;
+ // Concatting the empty string to the start fixes a very odd
+ // intermittent bug with IE9/10.
+ // On some occasions, without it, it will end up generating
+ // raw.foo.bar.baz && raw.foo.bar.baz && raw.foo.bar.baz
+ // At this point, not really sure why forcibly casting it to a string
+ // makes a difference
+ parts.push('' + current);
+ part = '';
+ }
+ }
+ }
+ result = parts.join(' && ');
+ result = Ext.functionFactory('raw', 'return ' + result);
+ }
+ return result;
+ };
+ }()),
+ /**
+ * @private
+ * @method
+ * Returns an accessor function for the passed Field. Gives support for properties such as
+ * the following:
+ *
+ * - 'someProperty'
+ * - 'some.property'
+ * - '["someProperty"]'
+ * - 'values[0]'
+ *
+ * This is used by {@link #buildExtractors} to create optimized extractor expressions when
+ * converting raw data into model instances. This method is used at the field level to
+ * dynamically map values to model fields.
+ */
+ createFieldAccessor: function(field) {
+ // Need to capture me for the extractor
+ var me = this,
+ mapping = field.mapping,
+ hasMap = mapping || mapping === 0,
+ map = hasMap ? mapping : field.name;
+ if (hasMap) {
+ if (typeof map === 'function') {
+ return function(raw, self) {
+ return field.mapping(raw, self);
+ };
+ } else {
+ return me.createAccessor(map);
+ }
+ }
+ },
+ getAccessorKey: function(prop) {
+ var simple = this.getUseSimpleAccessors() ? 'simple' : '';
+ return this.callParent([
+ simple + prop
+ ]);
+ },
+ privates: {
+ copyFrom: function(reader) {
+ this.callParent([
+ reader
+ ]);
+ this.getRoot = reader.getRoot;
+ },
+ setupExtractor: function(prop, defaultFn) {
+ return prop ? this.getAccessor(prop) : defaultFn;
+ }
+ }
+});
+
+/**
+ * This class is used to write {@link Ext.data.Model} data to the server in a JSON format.
+ * The {@link #allowSingle} configuration can be set to false to force the records to always
+ * be encoded in an array, even if there is only a single record being sent.
+ */
+Ext.define('Ext.data.writer.Json', {
+ extend: Ext.data.writer.Writer,
+ alternateClassName: 'Ext.data.JsonWriter',
+ alias: 'writer.json',
+ config: {
+ /**
+ * @cfg {String} rootProperty The HTTP parameter name by which JSON encoded records
+ * will be passed to the server if the {@link #encode} option is `true`.
+ */
+ rootProperty: undefined,
+ /**
+ * @cfg {Boolean} [encode=false] Configure `true` to send record data (all record fields
+ * if {@link #writeAllFields} is `true`) as a JSON encoded HTTP parameter named by the
+ * {@link #rootProperty} configuration.
+ *
+ * The encode option should only be set to true when a {@link #rootProperty} is defined,
+ * because the values will be sent as part of the request parameters as opposed to
+ * a raw post. The root will be the name of the parameter sent to the server.
+ */
+ encode: false,
+ /**
+ * @cfg {Boolean} [allowSingle=true] Configure with `false` to ensure that records are
+ * always wrapped in an array, even if there is only one record being sent. When there
+ * is more than one record, they will always be encoded into an array.
+ */
+ allowSingle: true,
+ /**
+ * @cfg {Boolean} [expandData=false] By default, when dot-delimited field
+ * {@link #nameProperty mappings} are used (e.g. `name: 'myProperty', mapping:
+ * 'my.nested.property'`) the writer will simply output a flat data object containing
+ * the mapping string literal as the property name (e.g. `{ 'my.nested.property': 'foo' }`).
+ *
+ * Mappings are used to map incoming nested JSON to flat Ext models. In many case, the data
+ * output by the writer should preferrably match the original nested data format. Setting
+ * this config to `true` will ensure that the output will instead look like
+ * `{ my: { nested: { property: 'foo' }}}`. The output is generated by
+ * {@link #getExpandedData}, which can optionally be overridden to apply
+ * more customized logic.
+ */
+ expandData: false
+ },
+ constructor: function(config) {
+ if (config && config.hasOwnProperty('root')) {
+ config = Ext.apply({}, config);
+ config.rootProperty = config.root;
+ delete config.root;
+ Ext.log.warn('Ext.data.writer.Json: Using the deprecated "root" configuration. ' + 'Use "rootProperty" instead.');
+ }
+ this.callParent([
+ config
+ ]);
+ },
+ /**
+ * @protected
+ * The Reader classes support dot-delimited data mappings for extracting nested raw data
+ * into fields, so the writer must support converting the flat {@link Ext.data.Model} structure
+ * back into the original nested data format. Using the same mappings when available, the Writer
+ * will simply split each delimiter into a nested object in the output, which should exactly
+ * match the input format. For example, record data like this:
+ *
+ * my.nested.property: 'foo',
+ * my.nested.another: 'bar',
+ * my.somethingElse: 123
+ *
+ * should write out as...
+ *
+ * my: {
+ * nested: {
+ * property: 'foo',
+ * another: 'bar
+ * },
+ * somethingElse: 123
+ * }
+ *
+ * This behavior is governed by the {@link #expandData} config. By default, this option is
+ * `false` for compatibility reasons, and will output a flat structure matching the flat record
+ * format. Setting this config to `true` will enable the expanded mapping behavior as shown
+ * here. This method could also be overridden to provide an even more customized output data
+ * structure.
+ */
+ getExpandedData: function(data) {
+ var dataLength = data.length,
+ i = 0,
+ item, prop, nameParts, j, tempObj, toObject;
+ toObject = function(name, value) {
+ var o = {};
+ o[name] = value;
+ return o;
+ };
+ for (; i < dataLength; i++) {
+ item = data[i];
+ for (prop in item) {
+ if (item.hasOwnProperty(prop)) {
+ // e.g. my.nested.property: 'foo'
+ nameParts = prop.split('.');
+ j = nameParts.length - 1;
+ if (j > 0) {
+ // Initially this will be the value 'foo'.
+ // Equivalent to rec['my.nested.property']
+ tempObj = item[prop];
+ for (; j > 0; j--) {
+ // Starting with the value above, we loop inside out, assigning the
+ // current object as the value for the parent name. Work all
+ // the way up until only the root name is left to assign.
+ tempObj = toObject(nameParts[j], tempObj);
+ }
+ // At this point we'll have all child properties rolled up into a single
+ // object like `{ nested: { property: 'foo' }}`. Now add the root name
+ // (e.g. 'my') to the record data if needed (do not overwrite existing):
+ item[nameParts[0]] = item[nameParts[0]] || {};
+ // Since there could be duplicate names at any level of the nesting be sure
+ // to merge rather than assign when setting the object as the value:
+ Ext.Object.merge(item[nameParts[0]], tempObj);
+ // Finally delete the original mapped property from the record
+ delete item[prop];
+ }
+ }
+ }
+ }
+ return data;
+ },
+ writeRecords: function(request, data) {
+ var me = this,
+ root = me.getRootProperty(),
+ json, single, transform;
+ if (me.getExpandData()) {
+ data = me.getExpandedData(data);
+ }
+ if (me.getAllowSingle() && data.length === 1) {
+ // convert to single object format
+ data = data[0];
+ single = true;
+ }
+ transform = this.getTransform();
+ if (transform) {
+ data = transform(data, request);
+ }
+ if (me.getEncode()) {
+ if (root) {
+ // sending as a param, need to encode
+ request.setParam(root, Ext.encode(data));
+ } else {
+ Ext.raise('Must specify a root when using encode');
+ }
+ } else if (single || (data && data.length)) {
+ // send as jsonData
+ json = request.getJsonData() || {};
+ if (root) {
+ json[root] = data;
+ } else {
+ json = data;
+ }
+ request.setJsonData(json);
+ }
+ return request;
+ }
+});
+
+/**
+ * This class provides a flexible means to control the
+ * `{@link Ext.util.Collection#cfg!sorters sorters}` of a
+ * `{@link Ext.util.Collection Collection}`. Instances of this class are created
+ * automatically when sorters are added to added to Collections.
+ *
+ * This collection can be directly manipulated by application code to gain full
+ * control over the sorters of the owner collection. Be aware that some components
+ * create sorters internally (such as grids) so be careful in such cases to not disturb
+ * these additional sorters.
+ *
+ * Items in this collection are `Ext.util.Sorter` instances and can be managed
+ * individually by their `id`. This is the recommended way to manage application
+ * filters while preserving sorter applied from other sources.
+ *
+ * Bulk changes to this collection should be wrapped in
+ * `{@link Ext.util.Collection#method!beginUpdate beginUpdate}` and
+ * `{@link Ext.util.Collection#method!endUpdate endUpdate}` (as with any collection).
+ * During these bulk updates all reactions to sorter changes will be suspended.
+ */
+Ext.define('Ext.util.SorterCollection', {
+ extend: Ext.util.Collection,
+ isSorterCollection: true,
+ /**
+ * @property {Ext.util.Sortable} sortable
+ * The owning sortable instance. The sortable's configuration governs this
+ * collection.
+ * @private
+ * @readonly
+ */
+ $sortable: null,
+ /**
+ * @property sortFn
+ * This is the cached sorting function which is a generated function that calls all the
+ * configured sorters in the correct order.
+ * @readonly
+ */
+ sortFn: null,
+ config: {
+ /**
+ * @cfg {Function} applySorterOptionsFn
+ * A template method that can be used to apply options to a sorter during creation
+ * @private
+ */
+ sorterOptionsFn: null,
+ /**
+ * @cfg {Object} applySorterOptionsScope
+ * The scope to execute the {@link #applySorterOptionsFn}
+ * @private
+ */
+ sorterOptionsScope: null
+ },
+ constructor: function(config) {
+ var me = this;
+ me.sortFn = Ext.util.Sorter.createComparator(me);
+ me.callParent([
+ config
+ ]);
+ me.setDecoder(me.decodeSorter);
+ },
+ addSort: function(property, direction, mode) {
+ var me = this,
+ count, index, limit, options, primary, sorter, sorters;
+ if (!property) {
+ // nothing specified so just trigger a sort...
+ me.beginUpdate();
+ me.endUpdate();
+ } else {
+ options = me.getOptions();
+ if (property instanceof Array) {
+ sorters = property;
+ mode = direction;
+ direction = null;
+ } else if (Ext.isString(property)) {
+ if (!(sorter = me.get(property))) {
+ sorters = [
+ {
+ property: property,
+ direction: direction || options.getDefaultSortDirection()
+ }
+ ];
+ } else {
+ sorters = [
+ sorter
+ ];
+ }
+ } else if (Ext.isFunction(property)) {
+ sorters = [
+ {
+ sorterFn: property,
+ direction: direction || options.getDefaultSortDirection()
+ }
+ ];
+ } else {
+ if (!Ext.isObject(property)) {
+ Ext.raise('Invalid sort descriptor: ' + property);
+ }
+ sorters = [
+ property
+ ];
+ mode = direction;
+ direction = null;
+ }
+ if (mode && !me._sortModes[mode]) {
+ Ext.raise('Sort mode should be "multi", "append", "prepend" or "replace", not "' + mode + '"');
+ }
+ mode = me._sortModes[mode || 'replace'];
+ primary = me.getAt(0);
+ count = me.length;
+ index = mode.append ? count : 0;
+ // We have multiple changes to make, so mark the sorters collection as updating
+ // before we start.
+ me.beginUpdate();
+ // Leverage the decode logic wired to the collection to up-convert sorters to
+ // real instances.
+ me.splice(index, mode.replace ? count : 0, sorters);
+ if (mode.multi) {
+ count = me.length;
+ limit = options.getMultiSortLimit();
+ if (count > limit) {
+ me.removeAt(limit, count);
+ }
+ }
+ // count will be truncated
+ if (sorter && direction) {
+ sorter.setDirection(direction);
+ } else if (index === 0 && primary && primary === me.getAt(0)) {
+ // If we just adjusted the sorters at the front and the primary sorter is
+ // still the primary sorter, toggle its direction:
+ primary.toggle();
+ }
+ me.endUpdate();
+ }
+ },
+ clear: function() {
+ // The owning Collection needs to have its onSortersEndUpdate called on sorter clear so that
+ // it clears its sorted flag.
+ this.beginUpdate();
+ this.callParent();
+ this.endUpdate(this.items);
+ },
+ /**
+ * Returns an up to date sort function.
+ * @return {Function} The sort function.
+ */
+ getSortFn: function() {
+ return this.sortFn;
+ },
+ /**
+ * Get the first matching sorter with a matching property.
+ * @param {String} prop The property name
+ * @return {Ext.util.Sorter} The sorter. `null` if not found.
+ * @private
+ */
+ getByProperty: function(prop) {
+ var items = this.items,
+ len = items.length,
+ i, item;
+ for (i = 0; i < len; ++i) {
+ item = items[i];
+ if (item.getProperty() === prop) {
+ return item;
+ }
+ }
+ return null;
+ },
+ //-------------------------------------------------------------------------
+ // Private
+ _sortModes: {
+ append: {
+ append: 1
+ },
+ multi: {
+ multi: 1
+ },
+ prepend: {
+ prepend: 1
+ },
+ replace: {
+ replace: 1
+ }
+ },
+ decodeSorter: function(sorter, xclass) {
+ var me = this,
+ options = me.getOptions(),
+ root = options.getRootProperty(),
+ sorterOptionsFn = me.getSorterOptionsFn(),
+ currentSorter, sorterConfig, type;
+ if (sorter.isSorter) {
+ if (!sorter.getRoot()) {
+ sorter.setRoot(root);
+ }
+ } else {
+ sorterConfig = {
+ direction: options.getDefaultSortDirection(),
+ root: root
+ };
+ type = typeof sorter;
+ // If we are dealing with a string we assume it is a property they want to sort on.
+ if (type === 'string') {
+ currentSorter = me.get(sorter);
+ if (currentSorter) {
+ return currentSorter;
+ }
+ sorterConfig.property = sorter;
+ }
+ // If it is a function, we assume its a sorting function.
+ else if (type === 'function') {
+ sorterConfig.sorterFn = sorter;
+ } else // If we are dealing with an object, we assume its a Sorter configuration. In
+ // this case we create an instance of Sorter passing this configuration.
+ {
+ // Finally we get to the point where it has to be invalid
+ if (!Ext.isObject(sorter)) {
+ Ext.raise('Invalid sorter specified: ' + sorter);
+ }
+ sorterConfig = Ext.apply(sorterConfig, sorter);
+ if (sorterConfig.fn) {
+ sorterConfig.sorterFn = sorterConfig.fn;
+ delete sorterConfig.fn;
+ }
+ }
+ // If a sorter config was created, make it an instance
+ sorter = Ext.create(xclass || Ext.util.Sorter, sorterConfig);
+ }
+ if (sorterOptionsFn) {
+ sorterOptionsFn.call(me.getSorterOptionsScope() || me, sorter);
+ }
+ return sorter;
+ },
+ setSorterConfigure: function(fn, scope) {
+ this.setSorterOptionsFn(fn);
+ this.setSorterOptionsScope(scope);
+ },
+ decodeRemoveItems: function(args, index) {
+ var me = this,
+ ret = (index === undefined) ? args : args[index];
+ if (!ret || !ret.$cloned) {
+ if (args.length > index + 1 || !Ext.isIterable(ret)) {
+ ret = Ext.Array.slice(args, index);
+ }
+ // eslint-disable-next-line vars-on-top
+ var currentSorters = me.items,
+ ln = ret.length,
+ remove = [],
+ i, item, n, sorter, type;
+ for (i = 0; i < ln; i++) {
+ sorter = ret[i];
+ if (sorter && sorter.isSorter) {
+ remove.push(sorter);
+ } else {
+ type = typeof sorter;
+ if (type === 'string') {
+ sorter = me.get(sorter);
+ if (sorter) {
+ remove.push(sorter);
+ }
+ } else if (type === 'function') {
+ for (n = currentSorters.length; n-- > 0; ) {
+ item = currentSorters[n];
+ if (item.getSorterFn() === sorter) {
+ remove.push(item);
+ }
+ }
+ } else {
+ Ext.raise('Invalid sorter specification: ' + sorter);
+ }
+ }
+ }
+ ret = remove;
+ ret.$cloned = true;
+ }
+ return ret;
+ },
+ getOptions: function() {
+ // Odd thing this. We need a Sortable to know how to manage our collection, but
+ // we may not have one. Of course as a Collection, we *are* one as well... just
+ // that is not really useful to sort the sorters themselves, but we do have the
+ // default options for Sortables baked in, so we'll do.
+ return this.$sortable || this;
+ }
+});
+
+/**
+ * This class provides a flexible means to control the
+ * `{@link Ext.util.Collection#cfg!filters filters}` of a
+ * `{@link Ext.util.Collection Collection}`. Instances of this class are created
+ * automatically when filters are added to added to Collections.
+ *
+ * This collection can be directly manipulated by application code to gain full
+ * control over the filters of the owner collection. Be aware that some components
+ * create filters internally (such as `Ext.form.field.ComboBox` and the
+ * `Ext.grid.filters.Filters` plugin) so be careful in such cases to not disturb
+ * these additional filters.
+ *
+ * Items in this collection are `Ext.util.Filter` instances and can be managed
+ * individually by their `id`. This is the recommended way to manage application
+ * filters while preserving filters applied from other sources.
+ *
+ * Bulk changes to this collection should be wrapped in
+ * `{@link Ext.util.Collection#method!beginUpdate beginUpdate}` and
+ * `{@link Ext.util.Collection#method!endUpdate endUpdate}` (as with any collection).
+ * During these bulk updates all reactions to filter changes will be suspended.
+ */
+Ext.define('Ext.util.FilterCollection', {
+ extend: Ext.util.Collection,
+ isFilterCollection: true,
+ /**
+ * @property {Ext.util.Collection} $filterable
+ * The owning filterable instance. The filterable's configuration governs this
+ * collection.
+ * @private
+ * @readonly
+ */
+ $filterable: null,
+ /**
+ * @property filterFn
+ * This is the cached filter function.
+ * @readonly
+ */
+ filterFn: null,
+ constructor: function(config) {
+ var me = this;
+ // Because this closure operates on the collection, we are able to use it for as
+ // long as we have the Collection instance.
+ me.filterFn = Ext.util.Filter.createFilterFn(me);
+ me.callParent([
+ config
+ ]);
+ me.setDecoder(me.decodeFilter);
+ },
+ /**
+ * This method will filter an array based on the currently configured `filters`.
+ * @param {Array} data The array you want to have filtered.
+ * @return {Array} The array you passed after it is filtered.
+ */
+ filterData: function(data) {
+ return this.filtered ? Ext.Array.filter(data, this.filterFn) : data;
+ },
+ /**
+ * Returns the filter function.
+ * @return {Function} The filter function.
+ */
+ getFilterFn: function() {
+ return this.filterFn;
+ },
+ isItemFiltered: function(item) {
+ return !this.filterFn(item);
+ },
+ /**
+ * returns the number of *enabled* filters in this `FilterCollection`
+ * @returns {Number}
+ */
+ getFilterCount: function() {
+ var filters = this.items,
+ len = filters.length,
+ i;
+ for (i = len - 1; i >= 0; i--) {
+ if (filters[i].getDisabled()) {
+ len--;
+ }
+ }
+ return len;
+ },
+ //-------------------------------------------------------------------------
+ // Private
+ decodeFilter: function(filter) {
+ var options = this.getOptions(),
+ filterRoot = options.getRootProperty(),
+ filterConfig;
+ if (filter.isFilter) {
+ if (filter.setRoot && !filter.getRoot()) {
+ filter.setRoot(filterRoot);
+ }
+ } else {
+ filterConfig = {
+ root: filterRoot
+ };
+ if (Ext.isFunction(filter)) {
+ filterConfig.filterFn = filter;
+ } else // If we are dealing with an object, we assume its a Filter configuration. In
+ // this case we create an instance of Ext.util.Filter passing the config.
+ {
+ // Finally we get to the point where it has to be invalid
+ if (!Ext.isObject(filter)) {
+ Ext.raise('Invalid filter specified: ' + filter);
+ }
+ filterConfig = Ext.apply(filterConfig, filter);
+ if (filterConfig.fn) {
+ filterConfig.filterFn = filterConfig.fn;
+ delete filterConfig.fn;
+ }
+ if (Ext.util.Filter.isInvalid(filterConfig)) {
+ return false;
+ }
+ }
+ filter = new Ext.util.Filter(filterConfig);
+ }
+ return filter;
+ },
+ decodeRemoveItems: function(args, index) {
+ var me = this,
+ ret = (index === undefined) ? args : args[index];
+ if (!ret.$cloned) {
+ if (args.length > index + 1 || !Ext.isIterable(ret)) {
+ ret = Ext.Array.slice(args, index);
+ }
+ // eslint-disable-next-line vars-on-top
+ var currentFilters = me.items,
+ ln = ret.length,
+ remove = [],
+ filter, i, isFunction, isProp, isString, item, match, n, type;
+ for (i = 0; i < ln; i++) {
+ filter = ret[i];
+ if (filter && filter.isFilter) {
+ remove.push(filter);
+ } else {
+ type = typeof filter;
+ isFunction = type === 'function';
+ isProp = filter.property !== undefined && filter.value !== undefined;
+ isString = type === 'string';
+ if (!isFunction && !isProp && !isString) {
+ Ext.raise('Invalid filter specification: ' + filter);
+ }
+ for (n = currentFilters.length; n-- > 0; ) {
+ item = currentFilters[n];
+ match = false;
+ if (isString) {
+ match = item.getProperty() === filter;
+ } else if (isFunction) {
+ match = item.getFilterFn() === filter;
+ } else if (isProp) {
+ match = item.getProperty() === filter.property && item.getValue() === filter.value;
+ }
+ if (match) {
+ remove.push(item);
+ }
+ }
+ }
+ }
+ ret = remove;
+ ret.$cloned = true;
+ }
+ return ret;
+ },
+ getOptions: function() {
+ // Odd thing this. We need a Filterable to know how to manage our collection, but
+ // we may not have one. Of course as a Collection, we *are* one as well... just
+ // that is not really useful to filter the filters themselves, but we do have the
+ // default options for Filterable baked in, so we'll do.
+ return this.$filterable || this;
+ }
+});
+
+/**
+ * @private
+ * A collection containing the result of applying grouping to the records in the store.
+ */
+Ext.define('Ext.util.GroupCollection', {
+ extend: Ext.util.Collection,
+ isGroupCollection: true,
+ config: {
+ grouper: null,
+ groupConfig: null,
+ itemRoot: null
+ },
+ observerPriority: -100,
+ emptyGroupRetainTime: 300000,
+ // Private timer to hang on to emptied groups. Milliseconds.
+ constructor: function(config) {
+ this.emptyGroups = {};
+ this.callParent([
+ config
+ ]);
+ this.on('remove', 'onGroupRemove', this);
+ },
+ /**
+ * Returns the `Ext.util.Group` associated with the given record.
+ *
+ * @param {Object} item The item for which the group is desired.
+ * @return {Ext.util.Group}
+ * @since 6.5.0
+ */
+ getItemGroup: function(item) {
+ var key = this.getGrouper().getGroupString(item);
+ return this.get(key);
+ },
+ //-------------------------------------------------------------------------
+ // Calls from the source Collection:
+ onCollectionAdd: function(source, details) {
+ if (!this.isConfiguring) {
+ this.addItemsToGroups(source, details.items, details.at);
+ }
+ },
+ onCollectionBeforeItemChange: function(source, details) {
+ this.changeDetails = details;
+ },
+ onCollectionBeginUpdate: function() {
+ this.beginUpdate();
+ },
+ onCollectionEndUpdate: function() {
+ this.endUpdate();
+ },
+ onCollectionItemChange: function(source, details) {
+ // Check if the change to the item caused the item to move. If it did, the group
+ // ordering will be handled by virtue of being removed/added to the collection.
+ // If not, check whether we're in the correct group and fix up if not.
+ if (!details.indexChanged) {
+ this.syncItemGrouping(source, details);
+ }
+ this.changeDetails = null;
+ },
+ onCollectionRefresh: function(source) {
+ if (source.generation) {
+ // eslint-disable-next-line vars-on-top
+ var me = this,
+ itemGroupKeys = me.itemGroupKeys = {},
+ groupData = me.createEntries(source, source.items),
+ entries = groupData.entries,
+ groupKey, i, len, entry, j;
+ // The magic of Collection will automatically update the group with its new
+ // members.
+ for (i = 0 , len = entries.length; i < len; ++i) {
+ entry = entries[i];
+ // Will add or replace
+ entry.group.splice(0, 1.0E99, entry.items);
+ // Add item key -> group mapping for every entry
+ for (j = 0; j < entry.items.length; j++) {
+ itemGroupKeys[source.getKey(entry.items[j])] = entry.group;
+ }
+ }
+ // Remove groups to which we have not added items.
+ entries = null;
+ for (groupKey in me.map) {
+ if (!(groupKey in groupData.groups)) {
+ (entries || (entries = [])).push(me.map[groupKey]);
+ }
+ }
+ if (entries) {
+ me.remove(entries);
+ }
+ // autoSort is disabled when adding new groups because
+ // it relies on there being at least one record in the group
+ me.sortItems();
+ }
+ },
+ onCollectionRemove: function(source, details) {
+ var me = this,
+ changeDetails = me.changeDetails,
+ itemGroupKeys = me.itemGroupKeys || (me.itemGroupKeys = {}),
+ entries, entry, group, i, n, j, removeGroups, item;
+ if (source.getCount()) {
+ if (changeDetails) {
+ // The item has changed, so the group key may be different, need
+ // to look it up
+ item = changeDetails.item || changeDetails.items[0];
+ entries = me.createEntries(source, [
+ item
+ ], false).entries;
+ entries[0].group = itemGroupKeys['oldKey' in details ? details.oldKey : source.getKey(item)];
+ } else {
+ entries = me.createEntries(source, details.items, false).entries;
+ }
+ for (i = 0 , n = entries.length; i < n; ++i) {
+ group = (entry = entries[i]).group;
+ if (group) {
+ group.remove(entry.items);
+ }
+ // Delete any item key -> group mapping
+ for (j = 0; j < entry.items.length; j++) {
+ delete itemGroupKeys[source.getKey(entry.items[j])];
+ }
+ if (group && !group.length) {
+ (removeGroups || (removeGroups = [])).push(group);
+ }
+ }
+ } else // Straight cleardown
+ {
+ me.itemGroupKeys = {};
+ removeGroups = me.items;
+ for (i = 0 , n = removeGroups.length; i < n; ++i) {
+ removeGroups[i].clear();
+ }
+ }
+ if (removeGroups) {
+ me.remove(removeGroups);
+ }
+ },
+ // If the SorterCollection instance is not changing, the Group will react to
+ // changes inside the SorterCollection, but if the instance changes we need
+ // to sync the Group to the new SorterCollection.
+ onCollectionSort: function(source) {
+ // sorting the collection effectively sorts the items in each group...
+ var me = this,
+ sorters = source.getSorters(false),
+ items, length, i, group;
+ if (sorters) {
+ items = me.items;
+ length = me.length;
+ for (i = 0; i < length; ++i) {
+ group = items[i];
+ if (group.getSorters() === sorters) {
+ group.sortItems();
+ } else {
+ group.setSorters(sorters);
+ }
+ }
+ }
+ },
+ onCollectionUpdateKey: function(source, details) {
+ if (!details.indexChanged) {
+ details.oldIndex = source.indexOf(details.item);
+ this.syncItemGrouping(source, details);
+ }
+ },
+ //-------------------------------------------------------------------------
+ // Private
+ addItemsToGroups: function(source, items, at, oldIndex) {
+ var me = this,
+ itemGroupKeys = me.itemGroupKeys || (me.itemGroupKeys = {}),
+ entries = me.createEntries(source, items).entries,
+ index = -1,
+ sourceStartIndex, entry, i, len, j, group, firstIndex, item;
+ for (i = 0 , len = entries.length; i < len; ++i) {
+ entry = entries[i];
+ group = entry.group;
+ // A single item moved - from onCollectionItemChange
+ if (oldIndex || oldIndex === 0) {
+ item = items[0];
+ if (group.getCount() > 0 && source.getSorters().getCount() === 0) {
+ // We have items in the group & it's not sorted, so find the
+ // correct position in the group to insert.
+ firstIndex = source.indexOf(group.items[0]);
+ if (oldIndex < firstIndex) {
+ index = 0;
+ } else {
+ index = oldIndex - firstIndex;
+ }
+ }
+ if (index === -1) {
+ group.add(item);
+ } else {
+ group.insert(index, item);
+ }
+ } else {
+ if (me.length > 1 && at) {
+ sourceStartIndex = source.indexOf(entries[0].group.getAt(0));
+ at = Math.max(at - sourceStartIndex, 0);
+ }
+ entry.group.insert(at != null ? at : group.items.length, entry.items);
+ // Add item key -> group mapping
+ for (j = 0; j < entry.items.length; j++) {
+ itemGroupKeys[source.getKey(entry.items[j])] = entry.group;
+ }
+ }
+ }
+ // autoSort is disabled when adding new groups because
+ // it relies on there being at least one record in the group
+ me.sortItems();
+ },
+ createEntries: function(source, items, createGroups) {
+ // Separate the items out into arrays by group
+ var me = this,
+ groups = {},
+ entries = [],
+ grouper = me.getGrouper(),
+ entry, group, groupKey, i, item, len;
+ for (i = 0 , len = items.length; i < len; ++i) {
+ groupKey = grouper.getGroupString(item = items[i]);
+ if (!(entry = groups[groupKey])) {
+ group = me.getGroup(source, groupKey, createGroups);
+ entries.push(groups[groupKey] = entry = {
+ group: group,
+ items: []
+ });
+ }
+ // Collect items to add/remove for each group
+ // which has items in the array
+ entry.items.push(item);
+ }
+ return {
+ groups: groups,
+ entries: entries
+ };
+ },
+ syncItemGrouping: function(source, details) {
+ var me = this,
+ itemGroupKeys = me.itemGroupKeys || (me.itemGroupKeys = {}),
+ item = details.item,
+ oldKey, itemKey, oldGroup, group;
+ itemKey = source.getKey(item);
+ oldKey = 'oldKey' in details ? details.oldKey : itemKey;
+ // The group the item was in before the change took place.
+ oldGroup = itemGroupKeys[oldKey];
+ // Look up/create the group into which the item now must be added.
+ group = me.getGroup(source, me.getGrouper().getGroupString(item));
+ details.group = group;
+ details.oldGroup = oldGroup;
+ // The change did not cause a change in group
+ if (!(details.groupChanged = group !== oldGroup)) {
+ // Inform group about change
+ oldGroup.itemChanged(item, details.modified, details.oldKey, details);
+ } else {
+ // Remove from its old group if there was one.
+ if (oldGroup) {
+ // Ensure Geoup knows about any unknown key changes, or item will not be removed.
+ oldGroup.updateKey(item, oldKey, itemKey);
+ oldGroup.remove(item);
+ // Queue newly empy group for destruction.
+ if (!oldGroup.length) {
+ me.remove(oldGroup);
+ }
+ }
+ // Add to new group
+ me.addItemsToGroups(source, [
+ item
+ ], null, details.oldIndex);
+ }
+ // Keep item key -> group mapping up to date
+ delete itemGroupKeys[oldKey];
+ itemGroupKeys[itemKey] = group;
+ },
+ getGroup: function(source, key, createGroups) {
+ var me = this,
+ group = me.get(key),
+ autoSort = me.getAutoSort();
+ if (group) {
+ group.setSorters(source.getSorters());
+ } else if (createGroups !== false) {
+ group = me.emptyGroups[key] || Ext.create(Ext.apply({
+ xclass: 'Ext.util.Group',
+ id: me.getId() + '-group-' + key,
+ groupKey: key,
+ rootProperty: me.getItemRoot(),
+ sorters: source.getSorters()
+ }, me.getGroupConfig()));
+ group.ejectTime = null;
+ me.setAutoSort(false);
+ me.add(group);
+ me.setAutoSort(autoSort);
+ }
+ return group;
+ },
+ getKey: function(item) {
+ return item.getGroupKey();
+ },
+ createSortFn: function() {
+ var me = this,
+ grouper = me.getGrouper(),
+ sorterFn = me.getSorters().getSortFn();
+ if (!grouper) {
+ return sorterFn;
+ }
+ return function(lhs, rhs) {
+ // The grouper has come from the collection, so we pass the items in
+ // the group for comparison because the grouper is also used to
+ // sort the data in the collection
+ return grouper.sort(lhs.items[0], rhs.items[0]) || sorterFn(lhs, rhs);
+ };
+ },
+ updateGrouper: function(grouper) {
+ var me = this;
+ me.grouped = !!(grouper && me.$groupable.getAutoGroup());
+ me.onSorterChange();
+ me.onEndUpdateSorters(me.getSorters());
+ },
+ destroy: function() {
+ var me = this;
+ me.$groupable = null;
+ // Ensure group objects get destroyed, they may have
+ // added listeners to the main collection sorters.
+ me.destroyGroups(me.items);
+ Ext.undefer(me.checkRemoveQueueTimer);
+ me.callParent();
+ },
+ privates: {
+ destroyGroups: function(groups) {
+ var len = groups.length,
+ i;
+ for (i = 0; i < len; ++i) {
+ groups[i].destroy();
+ }
+ },
+ onGroupRemove: function(collection, info) {
+ var me = this,
+ groups = info.items,
+ emptyGroups = me.emptyGroups,
+ len, group, i;
+ groups = Ext.Array.from(groups);
+ for (i = 0 , len = groups.length; i < len; i++) {
+ group = groups[i];
+ group.setSorters(null);
+ emptyGroups[group.getGroupKey()] = group;
+ group.ejectTime = Ext.now();
+ }
+ // Removed empty groups are reclaimable by getGroup
+ // for emptyGroupRetainTime milliseconds
+ me.checkRemoveQueue();
+ },
+ checkRemoveQueue: function() {
+ var me = this,
+ emptyGroups = me.emptyGroups,
+ groupKey, group, reschedule;
+ for (groupKey in emptyGroups) {
+ group = emptyGroups[groupKey];
+ // If the group's retain time has expired, destroy it.
+ if (!group.getCount() && Ext.now() - group.ejectTime > me.emptyGroupRetainTime) {
+ Ext.destroy(group);
+ delete emptyGroups[groupKey];
+ } else {
+ reschedule = true;
+ }
+ }
+ // Still some to remove in the future. Check back in emptyGroupRetainTime
+ if (reschedule) {
+ Ext.undefer(me.checkRemoveQueueTimer);
+ me.checkRemoveQueueTimer = Ext.defer(me.checkRemoveQueue, me.emptyGroupRetainTime, me);
+ }
+ }
+ }
+});
+
+/**
+ * The Store class encapsulates a client side cache of {@link Ext.data.Model Model} objects.
+ * Stores load data via a {@link Ext.data.proxy.Proxy Proxy}, and also provide functions
+ * for {@link #method-sort sorting}, {@link #filter filtering} and querying the
+ * {@link Ext.data.Model model} instances contained within it.
+ *
+ * Creating a Store is easy - we just tell it the Model and the Proxy to use for loading and saving
+ * its data:
+ *
+ * // Set up a model to use in our Store
+ * Ext.define('User', {
+ * extend: 'Ext.data.Model',
+ * fields: [
+ * {name: 'firstName', type: 'string'},
+ * {name: 'lastName', type: 'string'},
+ * {name: 'age', type: 'int'},
+ * {name: 'eyeColor', type: 'string'}
+ * ]
+ * });
+ *
+ * var myStore = Ext.create('Ext.data.Store', {
+ * model: 'User',
+ * proxy: {
+ * type: 'ajax',
+ * url: '/users.json',
+ * reader: {
+ * type: 'json',
+ * rootProperty: 'users'
+ * }
+ * },
+ * autoLoad: true
+ * });
+ *
+ * In the example above we configured an AJAX proxy to load data from the url '/users.json'.
+ * We told our Proxy to use a {@link Ext.data.reader.Json JsonReader} to parse the response
+ * from the server into Model object - {@link Ext.data.reader.Json see the docs on JsonReader}
+ * for details.
+ *
+ * ## Inline data
+ *
+ * Stores can also load data inline. Internally, Store converts each of the objects we pass in
+ * as {@link #cfg-data} into Model instances:
+ *
+ * Ext.create('Ext.data.Store', {
+ * model: 'User',
+ * data : [
+ * {firstName: 'Peter', lastName: 'Venkman'},
+ * {firstName: 'Egon', lastName: 'Spengler'},
+ * {firstName: 'Ray', lastName: 'Stantz'},
+ * {firstName: 'Winston', lastName: 'Zeddemore'}
+ * ]
+ * });
+ *
+ * Loading inline data using the method above is great if the data is in the correct format already
+ * (e.g. it doesn't need to be processed by a {@link Ext.data.reader.Reader reader}). If your inline
+ * data requires processing to decode the data structure, use a
+ * {@link Ext.data.proxy.Memory MemoryProxy} instead (see the
+ * {@link Ext.data.proxy.Memory MemoryProxy} docs for an example).
+ *
+ * Additional data can also be loaded locally using {@link #method-add}.
+ *
+ * ## Dynamic Loading
+ *
+ * Stores can be dynamically updated by calling the {@link #method-load} method:
+ *
+ * store.load({
+ * params: {
+ * group: 3,
+ * type: 'user'
+ * },
+ * callback: function(records, operation, success) {
+ * // do something after the load finishes
+ * },
+ * scope: this
+ * });
+ *
+ * Here a bunch of arbitrary parameters is passed along with the load request and a callback
+ * function is set up to do something after the loading is over.
+ *
+ * ## Loading Nested Data
+ *
+ * Applications often need to load sets of associated data - for example a CRM system might load
+ * a User and her Orders. Instead of issuing an AJAX request for the User and a series of additional
+ * AJAX requests for each Order, we can load a nested dataset and allow the Reader to automatically
+ * populate the associated models. Below is a brief example, see the {@link Ext.data.reader.Reader}
+ * intro docs for a full explanation:
+ *
+ * var store = Ext.create('Ext.data.Store', {
+ * autoLoad: true,
+ * model: "User",
+ * proxy: {
+ * type: 'ajax',
+ * url: 'users.json',
+ * reader: {
+ * type: 'json',
+ * rootProperty: 'users'
+ * }
+ * }
+ * });
+ *
+ * Which would consume a response like this:
+ *
+ * {
+ * "users": [{
+ * "id": 1,
+ * "name": "Peter",
+ * "orders": [{
+ * "id": 10,
+ * "total": 10.76,
+ * "status": "invoiced"
+ * },{
+ * "id": 11,
+ * "total": 13.45,
+ * "status": "shipped"
+ * }]
+ * }]
+ * }
+ *
+ * See the {@link Ext.data.reader.Reader} intro docs for a full explanation.
+ *
+ * ## Filtering and Sorting
+ *
+ * Stores can be sorted and filtered - in both cases either remotely or locally. The
+ * {@link #cfg-sorters} and {@link #cfg-filters} are held inside
+ * {@link Ext.util.Collection Collection} instances to make them easy to manage. Usually it is
+ * sufficient to either just specify sorters and filters in the Store configuration or call
+ * {@link #method-sort} or {@link #filter}:
+ *
+ * var store = Ext.create('Ext.data.Store', {
+ * model: 'User',
+ * sorters: [{
+ * property: 'age',
+ * direction: 'DESC'
+ * }, {
+ * property: 'firstName',
+ * direction: 'ASC'
+ * }],
+ *
+ * filters: [{
+ * property: 'firstName',
+ * value: /Peter/
+ * }]
+ * });
+ *
+ * The new Store will keep the configured sorters and filters in the Collection instances mentioned
+ * above. By default, sorting and filtering are both performed locally by the Store - see
+ * {@link #remoteSort} and {@link #remoteFilter} to allow the server to perform these operations
+ * instead.
+ *
+ * Filtering and sorting after the Store has been instantiated is also easy. Calling {@link #filter}
+ * adds another filter to the Store and automatically filters the dataset (calling {@link #filter}
+ * with no arguments simply re-applies all existing filters).
+ *
+ * store.filter('eyeColor', 'Brown');
+ *
+ * Change the sorting at any time by calling {@link #method-sort}:
+ *
+ * store.sort('height', 'ASC');
+ *
+ * Note that all existing sorters will be removed in favor of the new sorter data (if
+ * {@link #method-sort} is called with no arguments, the existing sorters are just reapplied
+ * instead of being removed). To keep existing sorters and add new ones, just add them to the
+ * Collection:
+ *
+ * store.sorters.add(new Ext.util.Sorter({
+ * property : 'shoeSize',
+ * direction: 'ASC'
+ * }));
+ *
+ * store.sort();
+ *
+ * ## Registering with StoreManager
+ *
+ * Any Store that is instantiated with a {@link #storeId} will automatically be registered with
+ * the {@link Ext.data.StoreManager StoreManager}. This makes it easy to reuse the same store
+ * in multiple views:
+ *
+ * //this store can be used several times
+ * Ext.create('Ext.data.Store', {
+ * model: 'User',
+ * storeId: 'usersStore'
+ * });
+ *
+ * new Ext.List({
+ * store: 'usersStore',
+ * //other config goes here
+ * });
+ *
+ * new Ext.view.View({
+ * store: 'usersStore',
+ * //other config goes here
+ * });
+ *
+ * ## Further Reading
+ *
+ * Stores are backed up by an ecosystem of classes that enables their operation. To gain a full
+ * understanding of these pieces and how they fit together, see:
+ *
+ * - {@link Ext.data.proxy.Proxy Proxy} - overview of what Proxies are and how they are used
+ * - {@link Ext.data.Model Model} - the core class in the data package
+ * - {@link Ext.data.reader.Reader Reader} - used by any subclass of
+ * {@link Ext.data.proxy.Server ServerProxy} to read a response
+ */
+Ext.define('Ext.data.Store', {
+ extend: Ext.data.ProxyStore,
+ alias: 'store.store',
+ mixins: [
+ Ext.data.LocalStore
+ ],
+ config: {
+ /**
+ * @cfg {Object[]/Ext.data.Model[]} data
+ * Array of Model instances or data objects to load locally. See "Inline data"
+ * above for details.
+ */
+ data: undefined,
+ // undefined so the applier is always called
+ /**
+ * @cfg {Boolean} [clearRemovedOnLoad=true]
+ * `true` to clear anything in the {@link #removed} record collection when the store loads.
+ */
+ clearRemovedOnLoad: true,
+ /**
+ * @cfg {Boolean} [clearOnPageLoad=true]
+ * True to empty the store when loading another page via {@link #loadPage},
+ * {@link #nextPage} or {@link #previousPage}. Setting to false keeps existing records,
+ * allowing large data sets to be loaded one page at a time but rendered all together.
+ */
+ clearOnPageLoad: true,
+ /**
+ * @cfg {Ext.data.Model} [associatedEntity]
+ * The owner of this store if the store is used as part of an association.
+ *
+ * @private
+ */
+ associatedEntity: null,
+ /**
+ * @cfg {Ext.data.schema.Role} [role]
+ * The role for the {@link #associatedEntity}.
+ *
+ * @private
+ */
+ role: null,
+ /**
+ * @cfg {Ext.data.Session} session
+ * The session for this store. By specifying a session, it ensures any records that are
+ * added to this store are also included in the session. This store does not become a member
+ * of the session itself.
+ *
+ * @since 5.0.0
+ */
+ session: null
+ },
+ /**
+ * @property {Ext.util.Collection} data
+ * The `data` property is a `Collection` which holds this store's local cache of records.
+ * @private
+ * @readonly
+ */
+ /**
+ * @private
+ * Used as a parameter to loadRecords
+ */
+ addRecordsOptions: {
+ addRecords: true
+ },
+ /**
+ * @property {Number} loadCount
+ * The number of times records have been loaded into the store. This includes loads via
+ * {@link #loadData} & {@link #loadRecords}.
+ * @readonly
+ */
+ loadCount: 0,
+ /**
+ * `true` once the store has loaded data from the server.
+ * @property {Boolean} complete
+ *
+ * @private
+ */
+ complete: false,
+ moveMapCount: 0,
+ /**
+ * Creates the store.
+ * @param {Object} [config] Config object.
+ */
+ constructor: function(config) {
+ var me = this,
+ data;
+ if (config) {
+ if (config.buffered) {
+ if (this.self !== Ext.data.Store) {
+ Ext.raise('buffered config not supported on derived Store classes. ' + 'Please derive from Ext.data.BufferedStore.');
+ }
+ // Hide this from Cmd
+ /* eslint-disable-next-line dot-notation */
+ return new Ext.data['BufferedStore'](config);
+ }
+ if (config.remoteGroup) {
+ Ext.log.warn('Ext.data.Store: remoteGroup has been removed. ' + 'Use remoteSort instead.');
+ }
+ }
+ /**
+ * @event beforeprefetch
+ * Fires before a prefetch occurs. Return `false` to cancel.
+ * @param {Ext.data.Store} this
+ * @param {Ext.data.operation.Operation} operation The associated operation.
+ */
+ /**
+ * @event groupchange
+ * Fired whenever the grouping in the grid changes.
+ * @param {Ext.data.Store} store The store.
+ * @param {Ext.util.Grouper} grouper The grouper object.
+ */
+ /**
+ * @event prefetch
+ * Fires whenever records have been prefetched.
+ * @param {Ext.data.Store} this
+ * @param {Ext.data.Model[]} records An array of records.
+ * @param {Boolean} successful `true` if the operation was successful.
+ * @param {Ext.data.operation.Operation} operation The associated operation.
+ */
+ /**
+ * @event filterchange
+ * Fired whenever the filter set changes.
+ * @param {Ext.data.Store} store The store.
+ * @param {Ext.util.Filter[]} filters The array of Filter objects.
+ */
+ me.callParent([
+ config
+ ]);
+ // See applyData for the details.
+ data = me.inlineData;
+ if (data) {
+ delete me.inlineData;
+ me.loadInlineData(data);
+ }
+ },
+ /**
+ * @method getData
+ * Returns the store's records.
+ *
+ * **Note:** If your store has been filtered, getData() will return a filtered
+ * collection. Use `getData().{@link Ext.util.Collection#getSource getSource()}` to
+ * fetch all unfiltered records.
+ *
+ * @return {Ext.util.Collection} An Ext.util.Collection of records
+ * (an empty Collection if no records are held by the store).
+ */
+ /**
+ * @method setData
+ * Loads an array of data directly into the Store.
+ *
+ * setData() is ideal if your data's format is already in its appropriate format (e.g. it
+ * doesn't need to be processed by a reader). If your data's structure requires processing,
+ * use a {@link Ext.data.proxy.Memory MemoryProxy} or {@link #loadRawData}.
+ *
+ * Use {@link #loadData}, {@link #method-add}, or {@link #insert} if records need to be
+ * appended to the current recordset.
+ *
+ * @param {Ext.data.Model[]/Object[]} data Array of data to load. Any non-model instances
+ * will be cast into model instances first.
+ */
+ applyData: function(data, dataCollection) {
+ // We bring up the Collection for records which forms the bottom of the config
+ // dependency graph. The appliers for "filters" and "sorters" depend on "data"
+ // and "remoteFilter" and "remoteSort" depend on both in their updaters.
+ var me = this;
+ // Ensure that we process our Model config first.
+ me.getFields();
+ me.getModel();
+ // We might be configured with a Collection instance
+ if (data && data.isCollection) {
+ data.setRootProperty('data');
+ dataCollection = data;
+ dataCollection.addObserver(this);
+ // Perform a load postprocess if the incoming collection is loaded.
+ if (data.getCount()) {
+ me.afterLoadRecords(data.items);
+ // This is not fired by afterLoadRecords because loadRecords
+ // which calls afterLoadRecords is a public API which simply adds some
+ // records. This situation here though, is anaologous to a load.
+ if (me.hasListeners.load) {
+ me.fireEvent('load', me, data.items, true);
+ }
+ }
+ } else {
+ if (!dataCollection) {
+ dataCollection = me.constructDataCollection();
+ }
+ if (data) {
+ if (me.isInitializing) {
+ // When data is configured on the instance of a Store we must wait for
+ // all the things to initialize (sorters, filters, groupers) so that we
+ // can properly process that data. All of those appliers, however, depend
+ // on the dataCollection (us) to get booted up first so we must defer
+ // this back to after initConfig. In previous versions this was hacked
+ // at by the constructor via "config.data" but "data" can also be set on
+ // the Ext.define level so best to pick it up here and store aside to be
+ // finished in the constructor.
+ me.inlineData = data;
+ } else {
+ // If we are not constructing the Store than a setData call needs to be
+ // equivalent to the legacy loadData method with respect to events that fire,
+ // etc.
+ me.loadData(data);
+ }
+ }
+ }
+ return dataCollection;
+ },
+ loadInlineData: function(data) {
+ var me = this,
+ proxy = me.getProxy();
+ if (proxy && proxy.isMemoryProxy) {
+ proxy.setData(data);
+ // Allow a memory proxy to trigger a load initially
+ me.suspendEvents();
+ me.read();
+ me.resumeEvents();
+ } else {
+ // We make it silent because we don't want to fire a refresh event
+ me.removeAll(true);
+ // We don't want to fire addrecords event since we will be firing
+ // a refresh event later which will already take care of updating
+ // any views bound to this store
+ me.suspendEvents();
+ me.loadData(data);
+ me.resumeEvents();
+ }
+ },
+ /**
+ * @method insert
+ * @inheritdoc Ext.data.LocalStore#insert
+ */
+ onCollectionAdd: function(collection, info) {
+ this.loadCount = this.loadCount || 1;
+ this.onCollectionAddItems(collection, info.items, info);
+ },
+ onCollectionFilterAdd: function(collection, items) {
+ this.onCollectionAddItems(collection, items);
+ },
+ onCollectionAddItems: function(collection, records, info) {
+ var me = this,
+ len = records.length,
+ lastChunk = info ? !info.next : false,
+ // Must use class-specific removed property.
+ // Regular Stores add to the "removed" property on remove.
+ // TreeStores are having records removed all the time; node collapse removes.
+ // TreeStores add to the "removedNodes" property onNodeRemove
+ removed = me.removed,
+ ignoreAdd = me.ignoreCollectionAdd,
+ session = me.getSession(),
+ replaced = info && info.replaced,
+ i, sync, record, replacedItems;
+ // Collection add changes the items reference of the collection, and that array
+ // object if directly referenced by Ranges. The ranges have to refresh themselves
+ // upon add.
+ if (me.activeRanges) {
+ me.syncActiveRanges();
+ }
+ for (i = 0; i < len; ++i) {
+ record = records[i];
+ if (session) {
+ session.adopt(record);
+ }
+ // If ignoring, we don't want to do anything other than pull
+ // the added records into the session
+ if (!ignoreAdd) {
+ record.join(me);
+ if (removed && removed.length) {
+ Ext.Array.remove(removed, record);
+ }
+ sync = sync || record.phantom || record.dirty;
+ }
+ }
+ if (ignoreAdd) {
+ return;
+ }
+ if (replaced) {
+ replacedItems = [];
+ do {
+ Ext.Array.push(replacedItems, replaced.items);
+ replaced = replaced.next;
+ } while (replaced);
+ me.setMoving(replacedItems, true);
+ }
+ if (info) {
+ // If this is a replacement operation, there will have been a
+ // previous call to onCollectionRemove which will have fired no
+ // events in anticipation of a final refresh event.
+ // Here is where we inform interested parties of all the changes.
+ if (info.replaced) {
+ if (lastChunk) {
+ me.fireEvent('datachanged', me);
+ me.fireEvent('refresh', me);
+ }
+ } else {
+ me.fireEvent('add', me, records, info.at);
+ // If there is a next property, that means there is another range that needs
+ // to be removed after this. Wait until everything is gone before firing
+ // datachanged since it should be a bulk operation
+ if (lastChunk) {
+ me.fireEvent('datachanged', me);
+ }
+ }
+ }
+ if (replacedItems) {
+ me.setMoving(replacedItems, false);
+ }
+ // Addition means a sync is needed.
+ me.needsSync = me.needsSync || sync;
+ },
+ onCollectionBeforeItemChange: function(collection, info) {
+ var record = info.item,
+ modifiedFieldNames = info.modified || null,
+ type = info.meta;
+ // This is currently intended to be private
+ this.fireEvent('beforeupdate', this, record, type, modifiedFieldNames, info);
+ },
+ // If our source collection informs us that a filtered out item has changed, we must still
+ // fire the events...
+ onCollectionFilteredItemChange: function() {
+ this.onCollectionItemChange.apply(this, arguments);
+ },
+ onCollectionItemChange: function(collection, info) {
+ var me = this,
+ record = info.item,
+ modifiedFieldNames = info.modified || null,
+ type = info.meta;
+ if (me.fireChangeEvent(record)) {
+ // Inform any interested parties that a record has been mutated.
+ // This will be invoked on TreeStores in which the invoking record
+ // is an descendant of a collapsed node, and so *will not be contained by this store
+ me.onUpdate(record, type, modifiedFieldNames, info);
+ me.fireEvent('update', me, record, type, modifiedFieldNames, info);
+ me.fireEvent('datachanged', me);
+ }
+ },
+ afterChange: function(record, modifiedFieldNames, type) {
+ this.getData().itemChanged(record, modifiedFieldNames || null, undefined, type);
+ },
+ afterCommit: function(record, modifiedFieldNames) {
+ this.afterChange(record, modifiedFieldNames, Ext.data.Model.COMMIT);
+ },
+ afterEdit: function(record, modifiedFieldNames) {
+ this.needsSync = this.needsSync || record.dirty;
+ this.afterChange(record, modifiedFieldNames, Ext.data.Model.EDIT);
+ },
+ afterReject: function(record) {
+ this.afterChange(record, null, Ext.data.Model.REJECT);
+ },
+ afterDrop: function(record) {
+ this.getData().remove(record);
+ },
+ afterErase: function(record) {
+ this.removeFromRemoved(record);
+ },
+ /**
+ * @method add
+ * @inheritdoc Ext.data.LocalStore#add
+ */
+ /**
+ * (Local sort only) Inserts the passed Record into the Store at the index where it
+ * should go based on the current sort information.
+ *
+ * @param {Ext.data.Record} record
+ */
+ addSorted: function(record) {
+ var me = this,
+ remote = me.getRemoteSort(),
+ data = me.getData(),
+ index;
+ if (remote) {
+ data.setSorters(me.getSorters());
+ }
+ index = data.findInsertionIndex(record);
+ if (remote) {
+ data.setSorters(null);
+ }
+ return me.insert(index, record);
+ },
+ /**
+ * Removes the specified record(s) from the Store, firing the {@link #event-remove}
+ * event for the removed records.
+ *
+ * After all records have been removed a single `datachanged` is fired.
+ *
+ * @param {Ext.data.Model/Ext.data.Model[]/Number/Number[]} records Model instance or
+ * array of instances to remove or an array of indices from which to remove records.
+ * @param isMove (private)
+ * @param silent (private)
+ */
+ remove: function(records, isMove, silent) {
+ var me = this,
+ data = me.getDataSource(),
+ len, i, toRemove, record;
+ if (records) {
+ if (records.isModel) {
+ if (data.indexOf(records) > -1) {
+ toRemove = [
+ records
+ ];
+ len = 1;
+ } else {
+ len = 0;
+ }
+ } else {
+ toRemove = [];
+ for (i = 0 , len = records.length; i < len; ++i) {
+ record = records[i];
+ if (record && record.isEntity) {
+ if (!data.contains(record)) {
+
+ continue;
+ }
+ } else if (!(record = data.getAt(record))) {
+ // an index
+
+ continue;
+ }
+ toRemove.push(record);
+ }
+ len = toRemove.length;
+ }
+ }
+ if (!len) {
+ return [];
+ }
+ me.removeIsMove = isMove === true;
+ me.removeIsSilent = silent;
+ data.remove(toRemove);
+ me.removeIsSilent = false;
+ return toRemove;
+ },
+ onCollectionRemove: function(collection, info) {
+ var me = this,
+ // Must use class-specific removed property.
+ // Regular Stores add to the "removed" property on remove.
+ // TreeStores are having records removed all the time; node collapse removes.
+ // TreeStores add to the "removedNodes" property onNodeRemove
+ removed = me.removed,
+ records = info.items,
+ len = records.length,
+ index = info.at,
+ replacement = info.replacement,
+ /* eslint-disable-next-line max-len */
+ isMove = me.removeIsMove || (replacement && Ext.Array.equals(records, replacement.items)),
+ silent = me.removeIsSilent,
+ lastChunk = !info.next,
+ data = me.getDataSource(),
+ i, record;
+ if (me.ignoreCollectionRemove) {
+ return;
+ }
+ if (replacement) {
+ me.setMoving(replacement.items, true);
+ }
+ for (i = len - 1; i >= 0; i--) {
+ record = records[i];
+ // If the data contains the record, that means the record is filtered out, so
+ // it's not being removed, nor should it be unjoined
+ if (!data.contains(record)) {
+ // Don't push interally moving, or phantom (client side only),
+ // erasing (informing server through its own proxy) records
+ if (removed && !isMove && !record.phantom && !record.erasing) {
+ // Store the index the record was removed from so that rejectChanges can
+ // re-insert at the correct place.
+ // The record's index property won't do, as that is the index in the overall
+ // dataset when Store is buffered.
+ record.removedFrom = index + i;
+ removed.push(record);
+ // Removal of a non-phantom record which is NOT erasing (informing the server
+ // through its own proxy) requires that the store be synced at some point.
+ me.needsSync = true;
+ } else {
+ // Only unjoin if we're not being pushed into the removed collection. We still
+ // have an interest in that record otherwise.
+ record.unjoin(me);
+ }
+ }
+ }
+ if (!silent) {
+ // If this removal is just the first part of a replacement operation,
+ // do not fire the events now.
+ //
+ // onCollectionAddItems will fire a refresh event, and convert multiple
+ // remove and add operations to an atomic refresh event.
+ // This will provide a better UI update.
+ // Also, focus can only be preserved around one operation, so
+ // editing a field which is the sorted field could result in
+ // incorrect focus..
+ if (!replacement || !replacement.items.length) {
+ me.fireEvent('remove', me, records, index, isMove);
+ // If there is a next property, that means there is another range that needs
+ // to be removed after this. Wait until everything is gone before firing datachanged
+ // since it should be a bulk operation
+ if (lastChunk) {
+ me.fireEvent('datachanged', me);
+ }
+ }
+ }
+ if (replacement) {
+ me.setMoving(replacement.items, false);
+ }
+ },
+ onFilterEndUpdate: function() {
+ var me = this;
+ if (me.destroying || me.destroyed) {
+ return;
+ }
+ // Filtering changes the items reference of the collection, and that array
+ // object if directly referenced by Ranges. The ranges have to refresh themselves
+ // upon add.
+ if (me.activeRanges) {
+ me.syncActiveRanges();
+ }
+ me.callParent(arguments);
+ me.callObservers('Filter');
+ },
+ /**
+ * Removes the model instance(s) at the given index
+ * @param {Number} index The record index
+ * @param {Number} [count=1] The number of records to delete
+ */
+ removeAt: function(index, count) {
+ var data = this.getData();
+ // Sanity check input.
+ index = Math.max(index, 0);
+ if (index < data.length) {
+ if (arguments.length === 1) {
+ count = 1;
+ } else if (!count) {
+ return;
+ }
+ data.removeAt(index, count);
+ }
+ },
+ /**
+ * Removes all unfiltered items from the store. Filtered records will not be removed.
+ * Individual record `{@link #event-remove}` events are not fired by this method.
+ *
+ * @param {Boolean} [silent=false] Pass `true` to prevent the `{@link #event-clear}` event
+ * from being fired.
+ * @return {Ext.data.Model[]} The removed records.
+ */
+ removeAll: function(silent) {
+ var me = this,
+ data = me.getData(),
+ records = data.getRange();
+ // We want to remove and mute any events here
+ if (data.length) {
+ // Explicit true here, we never want to fire remove events
+ me.removeIsSilent = true;
+ me.callObservers('BeforeRemoveAll');
+ data.removeAll();
+ me.removeIsSilent = false;
+ if (!silent) {
+ me.fireEvent('clear', me, records);
+ me.fireEvent('datachanged', me);
+ }
+ me.callObservers('AfterRemoveAll', [
+ !!silent
+ ]);
+ }
+ return records;
+ },
+ /**
+ * Make a set of records be current in the store. This means that unneeded records
+ * will be removed and new records will be added.
+ * @param {Ext.data.Model[]} records The records to be current in the store.
+ *
+ * @private
+ */
+ setRecords: function(records) {
+ var count = this.getCount();
+ ++this.loadCount;
+ if (count) {
+ this.getData().splice(0, count, records);
+ } else {
+ this.add(records);
+ }
+ },
+ /**
+ * This method is basically the same as the JavaScript Array splice method.
+ *
+ * Negative indexes are interpreted starting at the end of the collection. That is,
+ * a value of -1 indicates the last item, or equivalent to `length - 1`.
+ *
+ * @param {Number} index The index at which to add or remove items.
+ * @param {Number/Object[]} toRemove The number of items to remove or an array of the
+ * items to remove.
+ * @param {Object[]} [toAdd] The items to insert at the given `index`.
+ * @private
+ */
+ splice: function(index, toRemove, toAdd) {
+ return this.getData().splice(index, toRemove, toAdd);
+ },
+ /**
+ * @protected
+ * Called internally when a Proxy has completed a load request
+ */
+ onProxyLoad: function(operation) {
+ var me = this,
+ resultSet = operation.getResultSet(),
+ records = operation.getRecords(),
+ successful = operation.wasSuccessful();
+ if (me.destroyed) {
+ return;
+ }
+ if (resultSet) {
+ me.totalCount = resultSet.getTotal();
+ }
+ if (successful) {
+ records = me.processAssociation(records);
+ me.loadRecords(records, operation.getAddRecords() ? {
+ addRecords: true
+ } : undefined);
+ me.attachSummaryRecord(resultSet);
+ } else {
+ me.loading = false;
+ }
+ if (me.hasListeners.load) {
+ me.fireEvent('load', me, records, successful, operation);
+ }
+ me.callObservers('AfterLoad', [
+ records,
+ successful,
+ operation
+ ]);
+ },
+ onProxyWrite: function(operation) {
+ if (operation.wasSuccessful()) {
+ this.attachSummaryRecord(operation.getResultSet());
+ }
+ this.callParent([
+ operation
+ ]);
+ },
+ // private
+ filterDataSource: function(fn) {
+ var source = this.getDataSource(),
+ items = source.items,
+ len = items.length,
+ ret = [],
+ i;
+ for (i = 0; i < len; i++) {
+ if (fn.call(source, items[i])) {
+ ret.push(items[i]);
+ }
+ }
+ return ret;
+ },
+ getNewRecords: function() {
+ return this.filterDataSource(this.filterNew);
+ },
+ getRejectRecords: function() {
+ return this.filterDataSource(this.filterRejects);
+ },
+ getUpdatedRecords: function() {
+ return this.filterDataSource(this.filterUpdated);
+ },
+ /**
+ * Loads an array of data straight into the Store.
+ *
+ * Using this method is great if the data is in the correct format already (e.g. it doesn't
+ * need to be processed by a reader). If your data requires processing to decode the data
+ * structure, use a {@link Ext.data.proxy.Memory MemoryProxy} or {@link #loadRawData}.
+ *
+ * @param {Ext.data.Model[]/Object[]} data Array of data to load. Any non-model instances will
+ * be cast into model instances first.
+ * @param {Boolean} [append=false] `true` to add the records to the existing records in the
+ * store, `false` to remove the old ones first.
+ */
+ loadData: function(data, append) {
+ var me = this,
+ length = data.length,
+ newData = [],
+ i;
+ // make sure each data element is an Ext.data.Model instance
+ for (i = 0; i < length; i++) {
+ newData.push(me.createModel(data[i]));
+ }
+ newData = me.processAssociation(newData);
+ me.loadRecords(newData, append ? me.addRecordsOptions : undefined);
+ },
+ /**
+ * Loads data via the bound Proxy's reader
+ *
+ * Use this method if you are attempting to load data and want to utilize the configured data
+ * reader.
+ *
+ * As of 4.2, this method will no longer fire the {@link #event-load} event.
+ *
+ * @param {Object[]} data The full JSON object you'd like to load into the Data store.
+ * @param {Boolean} [append=false] `true` to add the records to the existing records in the
+ * store, `false` to remove the old ones first.
+ *
+ * @return {Boolean} `true` if the reader processed the records correctly. See
+ * {@link Ext.data.reader.Reader#successProperty}. If the reader did not process the records,
+ * nothing will be added.
+ */
+ loadRawData: function(data, append) {
+ var me = this,
+ session = me.getSession(),
+ result, records, success;
+ /* eslint-disable-next-line max-len */
+ result = me.getProxy().getReader().read(data, session ? {
+ recordCreator: session.recordCreator
+ } : undefined);
+ records = result.getRecords();
+ success = result.getSuccess();
+ if (success) {
+ me.totalCount = result.getTotal();
+ me.loadRecords(records, append ? me.addRecordsOptions : undefined);
+ }
+ return success;
+ },
+ /**
+ * Loads an array of {@link Ext.data.Model model} instances into the store, fires the
+ * datachanged event. This should only usually be called internally when loading from the
+ * {@link Ext.data.proxy.Proxy Proxy}, when adding records manually use {@link #method-add}
+ * instead
+ * @param {Ext.data.Model[]} records The array of records to load
+ * @param {Object} options
+ * @param {Boolean} [options.addRecords=false] Pass `true` to add these records to the existing
+ * records, `false` to remove the Store's existing records first.
+ */
+ loadRecords: function(records, options) {
+ var me = this,
+ data = me.getData(),
+ addRecords, skipSort;
+ if (options) {
+ addRecords = options.addRecords;
+ }
+ if (!me.getRemoteSort() && !me.getSortOnLoad()) {
+ skipSort = true;
+ data.setAutoSort(false);
+ }
+ if (!addRecords) {
+ me.clearData(true);
+ }
+ // Clear the flag AFTER the stores collection has been cleared down so that
+ // observers of that collection know that it was due to a load, and a refresh is imminent.
+ me.loading = false;
+ me.ignoreCollectionAdd = true;
+ me.callObservers('BeforePopulate');
+ data.add(records);
+ me.ignoreCollectionAdd = false;
+ if (skipSort) {
+ data.setAutoSort(true);
+ }
+ me.afterLoadRecords(records);
+ },
+ afterLoadRecords: function(records) {
+ var me = this,
+ length = records.length,
+ i;
+ for (i = 0; i < length; i++) {
+ records[i].join(me);
+ }
+ if (!me.isEmptyStore) {
+ ++me.loadCount;
+ me.complete = true;
+ }
+ if (me.hasListeners.datachanged) {
+ me.fireEvent('datachanged', me);
+ }
+ if (me.hasListeners.refresh) {
+ me.fireEvent('refresh', me);
+ }
+ me.callObservers('AfterPopulate');
+ },
+ // PAGING METHODS
+ /**
+ * Loads a given 'page' of data by setting the start and limit values appropriately. Internally
+ * this just causes a normal load operation, passing in calculated 'start' and 'limit' params.
+ * @param {Number} page The number of the page to load.
+ * @param {Object} [options] See options for {@link #method-load}.
+ */
+ loadPage: function(page, options) {
+ var me = this,
+ size = me.getPageSize();
+ me.currentPage = page;
+ // Copy options into a new object so as not to mutate passed in objects
+ options = Ext.apply({
+ page: page,
+ start: (page - 1) * size,
+ limit: size,
+ addRecords: !me.getClearOnPageLoad()
+ }, options);
+ me.read(options);
+ },
+ /**
+ * Loads the next 'page' in the current data set
+ * @param {Object} options See options for {@link #method-load}
+ */
+ nextPage: function(options) {
+ this.loadPage(this.currentPage + 1, options);
+ },
+ /**
+ * Loads the previous 'page' in the current data set
+ * @param {Object} options See options for {@link #method-load}
+ */
+ previousPage: function(options) {
+ this.loadPage(this.currentPage - 1, options);
+ },
+ /**
+ * @private
+ */
+ clearData: function(isLoad) {
+ var me = this,
+ removed = me.removed,
+ data = me.getDataSource(),
+ clearRemovedOnLoad = me.getClearRemovedOnLoad(),
+ needsUnjoinCheck = removed && isLoad && !clearRemovedOnLoad,
+ records, record, i, len;
+ // We only have to do the unjoining if not buffered. PageMap will unjoin its records when
+ // it clears itself.
+ // There is a potential for a race condition in stores configured with autoDestroy: true;
+ // if loading was initiated but didn't complete by the time the store is destroyed,
+ // the data MC may not have been created yet so we have to check for its existence
+ // here and below.
+ if (data) {
+ records = data.items;
+ for (i = 0 , len = records.length; i < len; ++i) {
+ record = records[i];
+ if (needsUnjoinCheck && Ext.Array.contains(removed, record)) {
+
+ continue;
+ }
+ record.unjoin(me);
+ }
+ me.ignoreCollectionRemove = true;
+ me.callObservers('BeforeClear');
+ data.removeAll();
+ me.ignoreCollectionRemove = false;
+ me.callObservers('AfterClear');
+ }
+ if (removed && (!isLoad || clearRemovedOnLoad)) {
+ removed.length = 0;
+ }
+ },
+ onIdChanged: function(rec, oldId, newId) {
+ this.getData().updateKey(rec, oldId);
+ // This event is used internally
+ this.fireEvent('idchanged', this, rec, oldId, newId);
+ },
+ /**
+ * Commits all Records with {@link #getModifiedRecords outstanding changes}. To handle updates
+ * for changes, subscribe to the Store's {@link #event-update update event}, and perform
+ * updating when the third parameter is Ext.data.Record.COMMIT.
+ */
+ commitChanges: function() {
+ var me = this,
+ recs = me.getModifiedRecords(),
+ len = recs.length,
+ i = 0;
+ Ext.suspendLayouts();
+ me.beginUpdate();
+ for (; i < len; i++) {
+ recs[i].commit();
+ }
+ me.cleanRemoved();
+ me.endUpdate();
+ Ext.resumeLayouts(true);
+ /**
+ * @private
+ * @event commit
+ * Fired when all changes were committed and the Store is clean.
+ *
+ * **Note** Used internally.
+ *
+ * @param {Ext.data.Store} store The Store object
+ */
+ me.fireEvent('commit', me);
+ },
+ filterNewOnly: function(item) {
+ return item.phantom === true;
+ },
+ filterRejects: function(item) {
+ return item.phantom || item.dirty;
+ },
+ /**
+ * {@link Ext.data.Model#reject Rejects} outstanding changes on all {@link #getModifiedRecords
+ * modified records} and re-insert any records that were removed locally. Any phantom records
+ * will be removed.
+ */
+ rejectChanges: function() {
+ var me = this,
+ recs = me.getRejectRecords(),
+ len = recs.length,
+ i, rec, toRemove, sorted, data, currentAutoSort;
+ Ext.suspendLayouts();
+ me.beginUpdate();
+ for (i = 0; i < len; i++) {
+ rec = recs[i];
+ if (rec.phantom) {
+ toRemove = toRemove || [];
+ toRemove.push(rec);
+ } else {
+ rec.reject();
+ }
+ }
+ if (toRemove) {
+ me.remove(toRemove);
+ for (i = 0 , len = toRemove.length; i < len; ++i) {
+ toRemove[i].reject();
+ }
+ }
+ // Restore removed records back to their original positions.
+ recs = me.getRawRemovedRecords();
+ if (recs) {
+ len = recs.length;
+ sorted = !me.getRemoteSort() && me.isSorted();
+ if (sorted) {
+ // Temporarily turn off sorting so .reject() doesn't attempt to sort the record.
+ // It would throw b/c the record isn't yet in its collection.
+ data = me.getData();
+ currentAutoSort = data.getAutoSort();
+ data.setAutoSort(false);
+ }
+ for (i = len - 1; i >= 0; i--) {
+ rec = recs[i];
+ rec.reject();
+ if (!sorted) {
+ me.insert(rec.removedFrom || 0, rec);
+ }
+ }
+ if (sorted) {
+ // Turn sorting back on so the collection is auto-sorted when added.
+ data.setAutoSort(currentAutoSort);
+ me.add(recs);
+ }
+ // Don't need to call cleanRemoved because we've re-added everything, don't
+ // need to unjoin the store
+ recs.length = 0;
+ }
+ me.endUpdate();
+ Ext.resumeLayouts(true);
+ /**
+ * @private
+ * @event reject
+ * Fired when all changes were rejected and the Store is clean.
+ *
+ * **Note** Used internally.
+ *
+ * @param {Ext.data.Store} store The Store object
+ */
+ me.fireEvent('reject', me);
+ },
+ doDestroy: function() {
+ var me = this,
+ task = me.loadTask,
+ data = me.getData(),
+ source = data.getSource();
+ // clearData ensures everything is unjoined
+ me.clearData();
+ me.setSession(null);
+ me.observers = null;
+ if (task) {
+ task.cancel();
+ me.loadTask = null;
+ }
+ if (source) {
+ source.destroy();
+ }
+ me.callParent();
+ },
+ privates: {
+ commitOptions: {
+ commit: true
+ },
+ attachSummaryRecord: function(resultSet) {
+ if (!resultSet) {
+ return;
+ }
+ /* eslint-disable-next-line vars-on-top */
+ var me = this,
+ summary = resultSet.getSummaryData(),
+ grouper = me.getGrouper(),
+ current = me.summaryRecord,
+ commitOptions = me.commitOptions,
+ groups, len, i, rec, group;
+ if (summary) {
+ if (current) {
+ current.set(summary.data, commitOptions);
+ } else {
+ me.summaryRecord = summary;
+ summary.isRemote = true;
+ }
+ }
+ if (grouper) {
+ summary = resultSet.getGroupData();
+ if (summary) {
+ groups = me.getGroups();
+ for (i = 0 , len = summary.length; i < len; ++i) {
+ rec = summary[i];
+ group = groups.getItemGroup(rec);
+ if (group) {
+ current = group.summaryRecord;
+ if (current) {
+ current.set(rec.data, commitOptions);
+ } else {
+ group.summaryRecord = rec;
+ rec.isRemote = true;
+ }
+ }
+ }
+ }
+ }
+ },
+ /**
+ * Similar to a load, however no records are added to the store. This is useful
+ * in allowing the developer to decide what to do with the new records.
+ * @param {Object} [options] See {@link #method-load load options}.
+ *
+ * @private
+ */
+ fetch: function(options) {
+ var operation;
+ options = Ext.apply({}, options);
+ this.setLoadOptions(options);
+ operation = this.createOperation('read', options);
+ operation.execute();
+ },
+ fireChangeEvent: function(record) {
+ return this.getDataSource().contains(record);
+ },
+ onBeforeLoad: function(operation) {
+ this.callObservers('BeforeLoad', [
+ operation
+ ]);
+ },
+ onRemoteFilterSet: function(filters, remoteFilter) {
+ if (filters) {
+ this.getData().setFilters(remoteFilter ? null : filters);
+ }
+ this.callParent([
+ filters,
+ remoteFilter
+ ]);
+ },
+ onRemoteSortSet: function(sorters, remoteSort) {
+ var data = this.getData();
+ if (sorters) {
+ data.setSorters(remoteSort ? null : sorters);
+ }
+ data.setAutoGroup(!remoteSort);
+ this.callParent([
+ sorters,
+ remoteSort
+ ]);
+ },
+ /**
+ * Checks whether records are being moved within the store. This can be used in conjunction
+ * with the {@link #event-add} and {@link #event-remove} events to determine whether
+ * the records are being removed/added or just having the position changed.
+ * @param {Ext.data.Model[]/Ext.data.Model} [records] The record(s).
+ * @param {Object} [getMap] (private)
+ * @return {Number} The number of records being moved. `0` if no records are moving.
+ * If records are passed the number will refer to how many of the passed records are moving.
+ *
+ * @private
+ */
+ isMoving: function(records, getMap) {
+ var map = this.moveMap,
+ moving = 0,
+ len, i;
+ if (map) {
+ if (records) {
+ if (Ext.isArray(records)) {
+ for (i = 0 , len = records.length; i < len; ++i) {
+ moving += map[records[i].id] ? 1 : 0;
+ }
+ } else if (map[records.id]) {
+ ++moving;
+ }
+ } else {
+ moving = getMap ? map : this.moveMapCount;
+ }
+ }
+ return moving;
+ },
+ setLoadOptions: function(options) {
+ // Only add grouping options if grouping is remote
+ var me = this,
+ pageSize = me.getPageSize(),
+ session, grouper;
+ if (me.getRemoteSort() && !options.grouper) {
+ grouper = me.getGrouper();
+ if (grouper) {
+ options.grouper = grouper;
+ }
+ }
+ if (pageSize || 'start' in options || 'limit' in options || 'page' in options) {
+ options.page = options.page != null ? options.page : me.currentPage;
+ options.start = (options.start !== undefined) ? options.start : (options.page - 1) * pageSize;
+ options.limit = options.limit != null ? options.limit : pageSize;
+ me.currentPage = options.page;
+ }
+ options.addRecords = options.addRecords || false;
+ if (!options.recordCreator) {
+ session = me.getSession();
+ if (session) {
+ options.recordCreator = session.recordCreator;
+ }
+ }
+ me.callParent([
+ options
+ ]);
+ },
+ setMoving: function(records, isMoving) {
+ var me = this,
+ map = me.moveMap || (me.moveMap = {}),
+ len = records.length,
+ i, id;
+ for (i = 0; i < len; ++i) {
+ id = records[i].id;
+ if (isMoving) {
+ if (map[id]) {
+ ++map[id];
+ } else {
+ map[id] = 1;
+ ++me.moveMapCount;
+ }
+ } else {
+ if (--map[id] === 0) {
+ delete map[id];
+ --me.moveMapCount;
+ }
+ }
+ }
+ if (me.moveMapCount === 0) {
+ me.moveMap = null;
+ }
+ },
+ processAssociation: function(records) {
+ var me = this,
+ associatedEntity = me.getAssociatedEntity();
+ if (associatedEntity) {
+ records = me.getRole().processLoad(me, associatedEntity, records, me.getSession());
+ }
+ return records;
+ }
+ }
+});
+// Provides docs from the mixin
+/**
+ * @method each
+ * @inheritdoc Ext.data.LocalStore#each
+ */
+/**
+ * @method collect
+ * @inheritdoc Ext.data.LocalStore#collect
+ */
+/**
+ * @method getById
+ * @inheritdoc Ext.data.LocalStore#getById
+ */
+/**
+ * @method getByInternalId
+ * @inheritdoc Ext.data.LocalStore#getByInternalId
+ */
+/**
+ * @method indexOf
+ * @inheritdoc Ext.data.LocalStore#indexOf
+ */
+/**
+ * @method indexOfId
+ * @inheritdoc Ext.data.LocalStore#indexOfId
+ */
+/**
+ * @method queryBy
+ * @inheritdoc Ext.data.LocalStore#queryBy
+ */
+/**
+ * @method query
+ * @inheritdoc Ext.data.LocalStore#query
+ */
+/**
+ * @method first
+ * @inheritdoc Ext.data.LocalStore#first
+ */
+/**
+ * @method last
+ * @inheritdoc Ext.data.LocalStore#last
+ */
+/**
+ * @method sum
+ * @inheritdoc Ext.data.LocalStore#sum
+ */
+/**
+ * @method count
+ * @inheritdoc Ext.data.LocalStore#count
+ */
+/**
+ * @method min
+ * @inheritdoc Ext.data.LocalStore#min
+ */
+/**
+ * @method max
+ * @inheritdoc Ext.data.LocalStore#max
+ */
+/**
+ * @method average
+ * @inheritdoc Ext.data.LocalStore#average
+ */
+/**
+ * @method aggregate
+ * @inheritdoc Ext.data.LocalStore#aggregate
+ */
+
+/**
+ * Data reader class to create an Array of {@link Ext.data.Model} objects from an Array.
+ * Each element of that Array represents a row of data fields. The
+ * fields are pulled into a Record object using as a subscript, the `mapping` property
+ * of the field definition if it exists, or the field's ordinal position in the definition.
+ *
+ * ##Example code:
+ *
+ * Employee = Ext.define('Employee', {
+ * extend: 'Ext.data.Model',
+ * fields: [
+ * 'id',
+ * // "mapping" only needed if an "id" field is present which
+ * // precludes using the ordinal position as the index.
+ * { name: 'name', mapping: 1 },
+ * { name: 'occupation', mapping: 2 }
+ * ]
+ * });
+ *
+ * var myReader = new Ext.data.reader.Array({
+ * model: 'Employee'
+ * }, Employee);
+ *
+ * This would consume an Array like this:
+ *
+ * [ [1, 'Bill', 'Gardener'], [2, 'Ben', 'Horticulturalist'] ]
+ *
+ */
+Ext.define('Ext.data.reader.Array', {
+ extend: Ext.data.reader.Json,
+ alternateClassName: 'Ext.data.ArrayReader',
+ alias: 'reader.array',
+ // For Array Reader, methods in the base which use these properties must not see the defaults
+ config: {
+ /**
+ * @cfg totalProperty
+ * @inheritdoc
+ */
+ totalProperty: undefined,
+ /**
+ * @cfg successProperty
+ * @inheritdoc
+ */
+ successProperty: undefined
+ },
+ /**
+ * @cfg {Boolean} preserveRawData
+ * @hide
+ */
+ /**
+ * @method constructor
+ * @constructor
+ * Create a new ArrayReader
+ * @param {Object} meta Metadata configuration options.
+ */
+ createFieldAccessor: function(field) {
+ // In the absence of a mapping property, use the original ordinal position
+ // at which the Model inserted the field into its collection.
+ var oldMap = field.mapping,
+ index = field.hasMapping() ? oldMap : field.ordinal,
+ result;
+ // Temporarily overwrite the mapping and use the superclass method.
+ field.mapping = index;
+ result = this.callParent(arguments);
+ field.mapping = oldMap;
+ return result;
+ },
+ getModelData: function(raw) {
+ // Can't preserve raw data here
+ return {};
+ }
+});
+
+/**
+ * Small helper class to make creating {@link Ext.data.Store}s from Array data easier. An
+ * ArrayStore will be automatically configured with a {@link Ext.data.reader.Array}.
+ *
+ * A store configuration would be something like:
+ *
+ * var store = Ext.create('Ext.data.ArrayStore', {
+ * // store configs
+ * storeId: 'myStore',
+ * // reader configs
+ * fields: [
+ * 'company',
+ * {name: 'price', type: 'float'},
+ * {name: 'change', type: 'float'},
+ * {name: 'pctChange', type: 'float'},
+ * {name: 'lastChange', type: 'date', dateFormat: 'n/j h:ia'}
+ * ]
+ * });
+ *
+ * This store is configured to consume a returned object of the form:
+ *
+ * var myData = [
+ * ['3m Co',71.72,0.02,0.03,'9/1 12:00am'],
+ * ['Alcoa Inc',29.01,0.42,1.47,'9/1 12:00am'],
+ * ['Boeing Co.',75.43,0.53,0.71,'9/1 12:00am'],
+ * ['Hewlett-Packard Co.',36.53,-0.03,-0.08,'9/1 12:00am'],
+ * ['Wal-Mart Stores, Inc.',45.45,0.73,1.63,'9/1 12:00am']
+ * ];
+ *
+ * An object literal of this form could also be used as the {@link #cfg-data} config option.
+ */
+Ext.define('Ext.data.ArrayStore', {
+ extend: Ext.data.Store,
+ alias: 'store.array',
+ alternateClassName: [
+ 'Ext.data.SimpleStore'
+ ],
+ config: {
+ proxy: {
+ type: 'memory',
+ reader: 'array'
+ }
+ },
+ loadData: function(data, append) {
+ if (this.expandData) {
+ /* eslint-disable-next-line vars-on-top */
+ var r = [],
+ i = 0,
+ ln = data.length;
+ for (; i < ln; i++) {
+ r[r.length] = [
+ data[i]
+ ];
+ }
+ data = r;
+ }
+ this.callParent([
+ data,
+ append
+ ]);
+ }
+});
+
+/**
+ * Contains a collection of all stores that are created that have an identifier. An identifier
+ * can be assigned by setting the {@link Ext.data.AbstractStore#storeId storeId} property. When
+ * a store is in the StoreManager, it can be referred to via it's identifier:
+ *
+ * Ext.create('Ext.data.Store', {
+ * model: 'SomeModel',
+ * storeId: 'myStore'
+ * });
+ *
+ * var store = Ext.data.StoreManager.lookup('myStore');
+ *
+ * Also note that the {@link #lookup} method is aliased to {@link Ext#getStore} for convenience.
+ *
+ * If a store is registered with the StoreManager, you can also refer to the store by its
+ * identifier when registering it with any Component that consumes data from a store:
+ *
+ * Ext.create('Ext.data.Store', {
+ * model: 'SomeModel',
+ * storeId: 'myStore'
+ * });
+ *
+ * Ext.create('Ext.view.View', {
+ * store: 'myStore',
+ * // other configuration here
+ * });
+ *
+ */
+Ext.define('Ext.data.StoreManager', {
+ extend: Ext.util.MixedCollection,
+ alternateClassName: [
+ 'Ext.StoreMgr',
+ 'Ext.data.StoreMgr',
+ 'Ext.StoreManager'
+ ],
+ singleton: true,
+ /**
+ * @cfg {Object} listeners
+ * @private
+ */
+ /**
+ * Registers one or more Stores with the StoreManager. You do not normally need to register
+ * stores manually. Any store initialized with a {@link Ext.data.Store#storeId} will be
+ * auto-registered.
+ * @param {Ext.data.Store...} stores Any number of Store instances
+ */
+ register: function() {
+ var i, s;
+ for (i = 0; (s = arguments[i]); i++) {
+ this.add(s);
+ }
+ },
+ /**
+ * Unregisters one or more Stores with the StoreManager
+ * @param {String/Object...} stores Any number of Store instances or ID-s
+ */
+ unregister: function() {
+ var i, s;
+ for (i = 0; (s = arguments[i]); i++) {
+ this.remove(this.lookup(s));
+ }
+ },
+ /**
+ * Gets a registered Store by id
+ * @param {String/Object} store The id of the Store, or a Store instance, or a store
+ * configuration
+ * @param {String} [defaultType] The store type to create when used with store configuration
+ * and there is no type specified on the config.
+ * @return {Ext.data.Store}
+ */
+ lookup: function(store, defaultType) {
+ var first, data, arrays, fields, i, len;
+ // handle the case when we are given an array or an array of arrays.
+ if (Ext.isArray(store)) {
+ first = store[0];
+ data = store;
+ if (Ext.isObject(first)) {
+ // store: [ { foo: 42, ... }, { foo: 427, ... }, ... ]
+ store = {
+ data: data
+ };
+ } else {
+ arrays = Ext.isArray(first);
+ fields = [
+ 'field1'
+ ];
+ if (arrays) {
+ // store: [ [1,2], [3,4], ... ]
+ for (i = 2 , len = first.length; i <= len; ++i) {
+ fields.push('field' + i);
+ }
+ } else {
+ // store: [ 1,2,3, ... ]
+ data = [];
+ for (i = 0 , len = store.length; i < len; ++i) {
+ data.push([
+ store[i]
+ ]);
+ }
+ }
+ return new Ext.data.ArrayStore({
+ data: data,
+ fields: fields,
+ autoDestroy: true,
+ autoCreated: true,
+ expanded: !arrays
+ });
+ }
+ }
+ if (Ext.isString(store)) {
+ // store id
+ return this.get(store);
+ } else {
+ // store instance or store config
+ return Ext.Factory.store(store, defaultType);
+ }
+ },
+ // getKey implementation for MixedCollection
+ getKey: function(o) {
+ return o.storeId;
+ },
+ addEmptyStore: function() {
+ // A dummy empty store with a fieldless Model defined in it.
+ // Just for binding to Views which are instantiated with no Store defined.
+ // They will be able to run and render fine, and be bound to a generated Store later.
+ var emptyStore = this.$emptyStore,
+ destoryable = {
+ destroy: Ext.emptyFn
+ };
+ if (!emptyStore) {
+ emptyStore = this.$emptyStore = Ext.regStore('ext-empty-store', {
+ proxy: 'memory',
+ useModelWarning: false
+ });
+ emptyStore.ignoreLeaked = true;
+ emptyStore.isEmptyStore = true;
+ emptyStore.on = emptyStore.addListener = function() {
+ return destoryable;
+ };
+ emptyStore.un = emptyStore.removeListener = Ext.emptyFn;
+ emptyStore.add = emptyStore.remove = emptyStore.insert = emptyStore.destroy = emptyStore.loadData = function() {
+ Ext.raise('Cannot modify ext-empty-store');
+ };
+ }
+ this.add(emptyStore);
+ },
+ clear: function() {
+ this.callParent();
+ this.addEmptyStore();
+ }
+}, function() {
+ /**
+ * Creates a new store for the given id and config, then registers it with the
+ * {@link Ext.data.StoreManager Store Manager}. Sample usage:
+ *
+ * Ext.regStore('AllUsers', {
+ * model: 'User'
+ * });
+ *
+ * // the store can now easily be used throughout the application
+ * new Ext.List({
+ * store: 'AllUsers',
+ * ... other config
+ * });
+ *
+ * @param {String/Object} id The id to set on the new store, or the `config` object
+ * that contains the `storeId` property.
+ * @param {Object} config The store config if the first parameter (`id`) is just the
+ * id.
+ * @member Ext
+ * @method regStore
+ */
+ Ext.regStore = function(id, config) {
+ var store;
+ if (Ext.isObject(id)) {
+ config = id;
+ } else {
+ if (Ext.data.StoreManager.containsKey(id)) {
+ return Ext.data.StoreManager.lookup(id);
+ }
+ config.storeId = id;
+ }
+ if (config instanceof Ext.data.Store) {
+ store = config;
+ } else {
+ store = new Ext.data.Store(config);
+ }
+ Ext.data.StoreManager.register(store);
+ return store;
+ };
+ /**
+ * @method getStore
+ * Shortcut to {@link Ext.data.StoreManager#lookup}.
+ * @member Ext
+ * @inheritdoc Ext.data.StoreManager#method-lookup
+ */
+ Ext.getStore = function(name) {
+ return Ext.data.StoreManager.lookup(name);
+ };
+ Ext.data.StoreManager.addEmptyStore();
+});
+
+/**
+ * This class implements the data store event domain. All classes extending from
+ * {@link Ext.data.AbstractStore} are included in this domain. The selectors are simply
+ * store id, alias or the wildcard "*" to match any store.
+ *
+ * @private
+ */
+Ext.define('Ext.app.domain.Store', {
+ extend: Ext.app.EventDomain,
+ singleton: true,
+ type: 'store',
+ prefix: 'store.',
+ idMatchRe: /^#/,
+ constructor: function() {
+ var me = this;
+ me.callParent();
+ me.monitor(Ext.data.AbstractStore);
+ },
+ match: function(target, selector) {
+ var result = false,
+ alias = target.alias;
+ if (selector === '*') {
+ result = true;
+ } else if (this.idMatchRe.test(selector)) {
+ result = target.getStoreId() === selector.substring(1);
+ } else if (alias) {
+ result = Ext.Array.indexOf(alias, this.prefix + selector) > -1;
+ }
+ return result;
+ }
+});
+
+/**
+ * Controllers are the glue that binds an application together. That said, their main
+ * purpose is to listen for events (usually from views) and take some action. Here's how
+ * we might create a Controller to manage Users:
+ *
+ * Ext.define('MyApp.controller.Users', {
+ * extend: 'Ext.app.Controller',
+ *
+ * init: function() {
+ * console.log('Initialized Users! This happens before ' +
+ * 'the Application launch() function is called');
+ * }
+ * });
+ *
+ * The init function is a special method that is called when your application boots. It is
+ * called before the {@link Ext.app.Application Application}'s launch function is executed.
+ * This creates an area you can run code prior to Viewport creation.
+ *
+ * The controller's {@link #method-control} function
+ * makes it easy to listen to events on your view classes and take some action with a
+ * handler function. Let's update our Users controller to tell us when the panel is
+ * rendered:
+ *
+ * Ext.define('MyApp.controller.Users', {
+ * extend: 'Ext.app.Controller',
+ *
+ * control: {
+ * 'viewport > panel': {
+ * render: 'onPanelRendered'
+ * }
+ * }
+ *
+ * onPanelRendered: function() {
+ * console.log('The panel was rendered');
+ * }
+ * });
+ *
+ * The {@link Ext.app.BaseController#method-control control method} has now set up
+ * listeners on views in our application. The control method uses the ComponentQuery
+ * engine to quickly and easily get references to components on the page. If you are not
+ * familiar with ComponentQuery yet, be sure to check out the
+ * {@link Ext.ComponentQuery documentation}. In brief, it allows us to pass a
+ * CSS-like selector that will find every matching component on the page.
+ *
+ * In our init function above, we supplied 'viewport > panel', which translates to "find me
+ * every Panel that is a direct child of a Viewport". We then supplied an object that maps
+ * event names (just 'render' in this case) to handler functions. In short, whenever
+ * a component that matches our selector fires a 'render' event, our
+ * onPanelRendered function is called.
+ *
+ * ## Event domains
+ *
+ * In Ext JS 4.2, we introduced the concept of event domains. In terms of MVC, an event
+ * domain is one or more base classes that fire events to which a Controller wants to
+ * listen. Besides Component event domain that encompass {@link Ext.Component}-descended
+ * Views, Controllers now can listen to events from data Stores, Ext Direct Providers,
+ * other Controllers, and Ext.GlobalEvents. This feature provides a way to communicate
+ * between parts of the whole application without the need to bind controllers together
+ * tightly, and allows to develop and test application parts in isolation.
+ *
+ * See usage examples in {@link #method-listen} method documentation.
+ *
+ * ## Using refs
+ *
+ * One of the most useful parts of Controllers is the ref system. These use
+ * {@link Ext.ComponentQuery} to make it really easy to get references to Views on your
+ * page. Let's look at an example of this now:
+ *
+ * Ext.define('MyApp.controller.Users', {
+ * extend: 'Ext.app.Controller',
+ *
+ * refs: [{
+ * ref: 'list',
+ * selector: 'grid'
+ * }],
+ *
+ * control: {
+ * 'button': {
+ * click: 'refreshGrid'
+ * }
+ * },
+ *
+ * refreshGrid: function() {
+ * this.getList().store.load();
+ * }
+ * });
+ *
+ * This example assumes the existence of a {@link Ext.grid.Panel Grid} on the page, which
+ * contains a single button to refresh the Grid when clicked. In our refs array, we set up
+ * a reference to the grid. There are two parts to this - the 'selector', which is a
+ * {@link Ext.ComponentQuery ComponentQuery} selector which finds any grid on the page and
+ * assigns it to the reference 'list'.
+ *
+ * By giving the reference a name, we get a number of things for free. The first is the
+ * getList function that we use in the refreshGrid method above. This is generated
+ * automatically by the Controller based on the name of our ref, which was capitalized and
+ * prepended with get to go from 'list' to 'getList'.
+ *
+ * The way this works is that the first time getList is called by your code, the
+ * ComponentQuery selector is run and the first component that matches the selector
+ * ('grid' in this case) will be returned. All future calls to getList will use a cached
+ * reference to that grid. Usually it is advised to use a specific ComponentQuery selector
+ * that will only match a single View in your application (in the case above our selector
+ * will match any grid on the page).
+ *
+ * Bringing it all together, we configure control
+ * to listen to any click on a {@link Ext.button.Button button} and call our refreshGrid
+ * function (again, this will match any button on the page so we advise a more specific
+ * selector than just 'button', but have left it this way for simplicity). When the button
+ * is clicked we use out getList function to refresh the grid.
+ *
+ * You can create any number of refs and control any number of components this way, simply
+ * adding more functions to your Controller as you go. For an example of real-world usage
+ * of Controllers see the Feed Viewer example in the examples/app/feed-viewer folder in
+ * the SDK download.
+ *
+ * ## Generated getter methods
+ *
+ * Refs aren't the only thing that generate convenient getter methods. Controllers often
+ * have to deal with Models and Stores so the framework offers a couple of easy ways to
+ * get access to those too. Let's look at another example:
+ *
+ * Ext.define('MyApp.controller.Users', {
+ * extend: 'Ext.app.Controller',
+ *
+ * models: ['User'],
+ * stores: ['AllUsers', 'AdminUsers'],
+ *
+ * init: function() {
+ * var User, allUsers, ed;
+ *
+ * User = this.getUserModel();
+ * allUsers = this.getAllUsersStore();
+ *
+ * ed = new User({ name: 'Ed' });
+ * allUsers.add(ed);
+ * }
+ * });
+ *
+ * By specifying Models and Stores that the Controller cares about, it again dynamically
+ * loads them from the appropriate locations (app/model/User.js, app/store/AllUsers.js and
+ * app/store/AdminUsers.js in this case) and creates getter functions for them all. The
+ * example above will create a new User model instance and add it to the AllUsers Store.
+ * Of course, you could do anything in this function but in this case we just did
+ * something simple to demonstrate the functionality.
+ *
+ * ## Further Reading
+ *
+ * For more information about writing Ext JS 5 applications, please see the
+ * [Application Architecture](../../../application_architecture/application_architecture.html).
+ * Also see the {@link Ext.app.Application} documentation.
+ */
+Ext.define('Ext.app.Controller', {
+ extend: Ext.app.BaseController,
+ statics: {
+ strings: {
+ model: {
+ getter: 'getModel',
+ upper: 'Model'
+ },
+ view: {
+ getter: 'getView',
+ upper: 'View'
+ },
+ controller: {
+ getter: 'getController',
+ upper: 'Controller'
+ },
+ store: {
+ getter: 'getStore',
+ upper: 'Store'
+ },
+ profile: {
+ getter: 'getProfile',
+ upper: 'Profiles'
+ }
+ },
+ controllerRegex: /^(.*)\.controller\./,
+ profileRegex: /^(.*)\.profile\./,
+ createGetter: function(baseGetter, name) {
+ return function() {
+ return this[baseGetter](name);
+ };
+ },
+ getGetterName: function(name, kindUpper) {
+ var fn = 'get',
+ parts = name.split('.'),
+ numParts = parts.length,
+ index;
+ // Handle namespaced class names. E.g. feed.Add becomes getFeedAddView etc.
+ for (index = 0; index < numParts; index++) {
+ fn += Ext.String.capitalize(parts[index]);
+ }
+ fn += kindUpper;
+ return fn;
+ },
+ resolveNamespace: function(cls, data) {
+ var Controller = Ext.app.Controller,
+ namespaceRe, className, namespace, match;
+ namespaceRe = cls.prototype.isProfile ? Controller.profileRegex : Controller.controllerRegex;
+ /*
+ * Namespace resolution is tricky business: we should know what namespace
+ * this Controller descendant belongs to, or model/store/view dependency
+ * resolution will be either ambiguous or plainly not possible. To avoid
+ * guessing games we try to look for a forward hint ($namespace) that
+ * Application class sets when its onClassExtended gets processed; if that
+ * fails we try to deduce namespace from class name.
+ *
+ * Note that for Ext.app.Application, Controller.onClassExtended gets executed
+ * *before* Application.onClassExtended so we have to delay namespace handling
+ * until after Application.onClassExtended kicks in, hence it is done in this hook.
+ */
+ className = Ext.getClassName(cls);
+ namespace = data.$namespace || data.namespace || Ext.app.getNamespace(className) || ((match = namespaceRe.exec(className)) && match[1]);
+ if (!namespace) {
+ Ext.log.warn("Missing namespace for " + className + ", please define it " + "in namespaces property of your Application class.");
+ }
+ return namespace;
+ },
+ /**
+ * This method is called like so:
+ *
+ * Ext.app.Controller.processDependencies(proto, requiresArray, 'MyApp', 'model', [
+ * 'User',
+ * 'Item',
+ * 'Foo@Common.model',
+ * 'Bar.Baz@Common.model'
+ * ]);
+ *
+ * Required dependencies are added to requiresArray.
+ *
+ * @private
+ */
+ processDependencies: function(cls, requires, namespace, kind, names, profileName) {
+ if (!names || !names.length) {
+ return;
+ }
+ /* eslint-disable-next-line vars-on-top */
+ var me = this,
+ strings = me.strings[kind],
+ o, absoluteName, shortName, name, j, subLn, getterName, getter;
+ if (!Ext.isArray(names)) {
+ names = [
+ names
+ ];
+ }
+ for (j = 0 , subLn = names.length; j < subLn; j++) {
+ name = names[j];
+ o = me.getFullName(name, kind, namespace, profileName);
+ // Update the name in the array to be the absolute name
+ names[j] = absoluteName = o.absoluteName;
+ shortName = o.shortName;
+ requires.push(absoluteName);
+ getterName = me.getGetterName(shortName, strings.upper);
+ if (!cls[getterName]) {
+ cls[getterName] = getter = me.createGetter(strings.getter, name);
+ } else if (getterName === 'getMainView') {
+ Ext.log.warn('Cannot have a view named \'Main\' - getter conflicts ' + 'with mainView config.');
+ }
+ // Application class will init the controller getters
+ if (getter && kind !== 'controller') {
+ // This marker allows the constructor to easily/cheaply identify the
+ // generated getter methods since they all need to be called to get
+ // things initialized. We use a property name that deliberately does
+ // not work with dot-access to reduce any chance of collision.
+ getter['Ext.app.getter'] = true;
+ }
+ }
+ },
+ getFullName: function(name, kind, namespace, profileName) {
+ var shortName = name,
+ sep, absoluteName;
+ if ((sep = name.indexOf('@')) > 0) {
+ // The unambiguous syntax is Model@Name.space (or "space.Model@Name")
+ // which contains both the short name ("Model" or "space.Model") and
+ // the full name (Name.space.Model).
+ //
+ shortName = name.substring(0, sep);
+ // "Model"
+ absoluteName = name.substring(sep + 1) + '.' + shortName;
+ }
+ // ex: "Name.space.Model"
+ // Deciding if a class name must be qualified:
+ //
+ // 1 - if the name doesn't contain a dot, we must qualify it
+ //
+ // 2 - the name may be a qualified name of a known class, but:
+ //
+ // 2.1 - in runtime, the loader may not know the class - specially in
+ // production - so we must check the class manager
+ //
+ // 2.2 - in build time, the class manager may not know the class, but
+ // the loader does, so we check the second one (the loader check
+ // assures it's really a class, and not a namespace, so we can
+ // have 'Books.controller.Books', and requesting a controller
+ // called Books will not be underqualified)
+ //
+ else if (name.indexOf('.') > 0 && (Ext.ClassManager.isCreated(name) || this.hasRegisteredPrefix(name))) {
+ absoluteName = name;
+ shortName = name.replace(namespace + '.' + kind + '.', '');
+ } else {
+ if (!namespace) {
+ Ext.log.warn("Cannot find namespace for " + kind + " " + name + ", " + "assuming it is fully qualified class name");
+ }
+ if (namespace) {
+ absoluteName = namespace + '.' + kind + '.' + (profileName ? profileName + '.' + name : name);
+ shortName = name;
+ } else {
+ absoluteName = name;
+ }
+ }
+ return {
+ absoluteName: absoluteName,
+ shortName: shortName
+ };
+ },
+ hasRegisteredPrefix: function(className) {
+ var inventory = Ext.ClassManager,
+ prefix = inventory.getPrefix(className);
+ // It's a class if className is not equal to any known namespace
+ return prefix && prefix !== className;
+ }
+ },
+ // @cmd-auto-dependency {aliasPrefix : "model.", mvc : true, blame: "all"}
+ /**
+ * @cfg {String/String[]} models
+ * Array of models to require from AppName.model namespace. For example:
+ *
+ * Ext.define("MyApp.controller.Foo", {
+ * extend: "Ext.app.Controller",
+ * models: ['User', 'Vehicle']
+ * });
+ *
+ * This is equivalent to:
+ *
+ * Ext.define("MyApp.controller.Foo", {
+ * extend: "Ext.app.Controller",
+ * requires: ['MyApp.model.User', 'MyApp.model.Vehicle'],
+ *
+ * getUserModel: function() {
+ * return this.getModel("User");
+ * },
+ *
+ * getVehicleModel: function() {
+ * return this.getModel("Vehicle");
+ * }
+ * });
+ *
+ * **Note:** If the model has a different namespace than that of the
+ * application you will need to specify the full class name as well as define a path
+ * in the {@link Ext.Loader#cfg-paths Loader's paths} config or
+ * {@link Ext.Loader#method-setPath setPath} method.
+ */
+ models: null,
+ // @cmd-auto-dependency {aliasPrefix: "view.", mvc: true, blame: "all"}
+ /**
+ * @cfg {String/String[]} views
+ * Array of views to require from AppName.view namespace and to generate getter methods for.
+ * For example:
+ *
+ * Ext.define("MyApp.controller.Foo", {
+ * extend: "Ext.app.Controller",
+ * views: ['List', 'Detail']
+ * });
+ *
+ * This is equivalent to:
+ *
+ * Ext.define("MyApp.controller.Foo", {
+ * extend: "Ext.app.Controller",
+ * requires: ['MyApp.view.List', 'MyApp.view.Detail'],
+ *
+ * getListView: function() {
+ * return this.getView("List");
+ * },
+ *
+ * getDetailView: function() {
+ * return this.getView("Detail");
+ * }
+ * });
+ *
+ * **Note:** If the view has a different namespace than that of the
+ * application you will need to specify the full class name as well as define a path
+ * in the {@link Ext.Loader#cfg-paths Loader's paths} config or
+ * {@link Ext.Loader#method-setPath setPath} method.
+ */
+ views: null,
+ // @cmd-auto-dependency {aliasPrefix: "store.", mvc: true, blame: "all"}
+ /**
+ * @cfg {String/String[]} stores
+ * Array of stores to require from AppName.store namespace and to generate getter methods for.
+ * For example:
+ *
+ * Ext.define("MyApp.controller.Foo", {
+ * extend: "Ext.app.Controller",
+ * stores: ['Users', 'Vehicles']
+ * });
+ *
+ * This is equivalent to:
+ *
+ * Ext.define("MyApp.controller.Foo", {
+ * extend: "Ext.app.Controller",
+ *
+ * requires: [
+ * 'MyApp.store.Users',
+ * 'MyApp.store.Vehicles'
+ * ]
+ *
+ * getUsersStore: function() {
+ * return this.getStore("Users");
+ * },
+ *
+ * getVehiclesStore: function() {
+ * return this.getStore("Vehicles");
+ * }
+ * });
+ *
+ * **Note:** If the store has a different namespace than that of the
+ * application you will need to specify the full class name as well as define a path
+ * in the {@link Ext.Loader#cfg-paths Loader's paths} config or
+ * {@link Ext.Loader#method-setPath setPath} method.
+ */
+ stores: null,
+ // @cmd-auto-dependency {aliasPrefix: "controller.", mvc: true, blame: "all"}
+ controllers: null,
+ config: {
+ /**
+ * @cfg {Ext.app.Application} application
+ * The {@link Ext.app.Application} for this controller accessible via the
+ * getApplication method.
+ * @accessor
+ * @readonly
+ */
+ application: null,
+ /**
+ * @cfg {Object/Object[]} refs
+ * @accessor
+ *
+ * The refs config creates a getter method on the controller that internally
+ * uses Ext.ComponentQuery to fetch the component instance using the configured
+ * selector. The following example will add the `getList` method to
+ * the controller and will return the first component in the application
+ * hierarchy with an xtype of "grid". By default, *undefined* will be returned
+ * when the query does not locate the target component.
+ *
+ * Ext.define('MyApp.controller.Foo', {
+ * extend: 'Ext.app.Controller',
+ *
+ * refs: [{
+ * ref: 'list',
+ * selector: 'grid'
+ * }]
+ * });
+ *
+ * The following fields may be used in the ref definition:
+ *
+ * - `ref` - name of the reference.
+ * - `selector` - Ext.ComponentQuery selector to access the component.
+ * - `autoCreate` - True to create the component automatically if not found on
+ * page.
+ * - `forceCreate` - True to force the creation of the component every time
+ * reference is accessed (when `get` is called).
+ * - `xtype` - Used to create the component by its xtype with `autoCreate` or
+ * `forceCreate`. If you don't provide `xtype`, an Ext.Component instance will
+ * be created.
+ *
+ * The following example will create a `getList` and `getUser` method on the
+ * controller.
+ *
+ * Ext.define('MyApp.controller.Foo', {
+ * extend: 'Ext.app.Controller',
+ *
+ * refs: [{
+ * list: 'grid',
+ * user: {
+ * autoCreate: true,
+ * selector: 'form',
+ * xtype: 'form'
+ * }
+ * }]
+ * });
+ */
+ refs: null,
+ active: true,
+ /**
+ * @private
+ */
+ moduleClassName: null
+ },
+ onClassExtended: function(cls, data, hooks) {
+ var onBeforeClassCreated = hooks.onBeforeCreated;
+ hooks.onBeforeCreated = function(cls, data) {
+ var Controller = Ext.app.Controller,
+ requires = [],
+ namespace, proto;
+ proto = cls.prototype;
+ namespace = Controller.resolveNamespace(cls, data);
+ if (namespace) {
+ proto.$namespace = namespace;
+ }
+ Controller.processDependencies(proto, requires, namespace, 'model', data.models);
+ Controller.processDependencies(proto, requires, namespace, 'view', data.views);
+ Controller.processDependencies(proto, requires, namespace, 'store', data.stores);
+ Controller.processDependencies(proto, requires, namespace, 'controller', data.controllers);
+ Ext.require(requires, Ext.Function.pass(onBeforeClassCreated, arguments, this));
+ };
+ },
+ /**
+ * Creates new Controller.
+ *
+ * @param {Object} [config] Configuration object.
+ */
+ constructor: function(config) {
+ this.initAutoGetters();
+ this.callParent([
+ config
+ ]);
+ },
+ /**
+ * @private
+ * Takes either an object and transforms it into an array. The following are valid
+ * refs values:
+ *
+ * refs: {
+ * myComponent: 'container'
+ * }
+ *
+ * refs: {
+ * myComponent: {
+ * selector: 'container'
+ * }
+ * }
+ *
+ * refs: [
+ * {
+ * ref: 'myComponent',
+ * selector: 'container'
+ * }
+ * ]
+ *
+ * @param {Array|Object} refs The refs to normalize
+ * @return {Array} The normalized array of refs
+ */
+ normalizeRefs: function(refs) {
+ var me = this,
+ newRefs = [];
+ if (refs) {
+ if (Ext.isObject(refs)) {
+ Ext.Object.each(refs, function(key, value) {
+ if (Ext.isString(value)) {
+ value = {
+ selector: value
+ };
+ }
+ value.ref = key;
+ newRefs.push(value);
+ });
+ } else if (Ext.isArray(refs)) {
+ newRefs = Ext.Array.merge(newRefs, refs);
+ }
+ }
+ refs = me.refs;
+ if (refs) {
+ me.refs = null;
+ refs = me.normalizeRefs(refs);
+ if (refs) {
+ newRefs = Ext.Array.merge(newRefs, refs);
+ }
+ }
+ return newRefs;
+ },
+ /**
+ * Returns a map of reference names to selectors
+ * @private
+ */
+ getRefMap: function() {
+ var me = this,
+ refMap = me._refMap,
+ refs, ref, ln, i;
+ if (!refMap) {
+ refs = me.getRefs();
+ refMap = me._refMap = {};
+ if (refs) {
+ for (i = 0 , ln = refs.length; i < ln; i++) {
+ ref = refs[i];
+ refMap[ref.ref] = ref.selector;
+ }
+ }
+ }
+ return refMap;
+ },
+ applyId: function(id) {
+ return id || Ext.app.Controller.getFullName(this.$className, 'controller', this.$namespace).shortName;
+ },
+ applyRefs: function(refs) {
+ return this.normalizeRefs(Ext.clone(refs));
+ },
+ /**
+ * @param {Object} refs The refs to pass to the {@link #ref} method.
+ * @private
+ */
+ updateRefs: function(refs) {
+ if (refs) {
+ this.ref(refs);
+ }
+ },
+ initAutoGetters: function() {
+ var proto = this.self.prototype,
+ prop, fn;
+ for (prop in proto) {
+ fn = proto[prop];
+ // Look for the marker placed on the getters by processDependencies so that
+ // we can know what to call cheaply:
+ if (fn && fn['Ext.app.getter']) {
+ fn.call(this);
+ }
+ }
+ },
+ doInit: function(app) {
+ var me = this;
+ if (!me._initialized) {
+ me.init(app);
+ me._initialized = true;
+ }
+ },
+ finishInit: function(app) {
+ var me = this,
+ controllers = me.controllers,
+ controller, i, l;
+ if (me._initialized && controllers && controllers.length) {
+ for (i = 0 , l = controllers.length; i < l; i++) {
+ controller = me.getController(controllers[i]);
+ controller.finishInit(app);
+ }
+ }
+ },
+ /**
+ * @method
+ *
+ * A template method that is called when your application boots. It is called before the
+ * {@link Ext.app.Application Application}'s launch function is executed so gives a hook point
+ * to run any code before your Viewport is created.
+ *
+ * @param {Ext.app.Application} application
+ *
+ * @template
+ */
+ init: Ext.emptyFn,
+ /**
+ * @method
+ *
+ * A template method like {@link #init}, but called after the viewport is created.
+ * This is called after the {@link Ext.app.Application#launch launch} method of Application
+ * is executed.
+ *
+ * @param {Ext.app.Application} application
+ *
+ * @template
+ */
+ onLaunch: Ext.emptyFn,
+ /**
+ * Allow the controller to resume receiving events from the event bus.
+ * Routes will also be able to begin firing on this controller.
+ * Also see {@link #deactivate}.
+ */
+ activate: function() {
+ this.setActive(true);
+ },
+ /**
+ * Prevent this controller from receiving events from the event bus.
+ * Routes will also not be triggered on inactive controllers unless
+ * the {@link Ext.route.Route#allowInactive} flag is set.
+ * Also see {@link #activate}.
+ */
+ deactivate: function() {
+ this.setActive(false);
+ },
+ /**
+ * Checks if this controller is active. See {@link #activate} &
+ * {@link #deactivate}.
+ * @return {Boolean} `true` if this controller is active.
+ */
+ isActive: function() {
+ return this.getActive();
+ },
+ ref: function(refs) {
+ var me = this,
+ i = 0,
+ length = refs.length,
+ info, ref, fn;
+ refs = Ext.Array.from(refs);
+ me.references = me.references || [];
+ for (; i < length; i++) {
+ info = refs[i];
+ ref = info.ref;
+ fn = 'get' + Ext.String.capitalize(ref);
+ if (!me[fn]) {
+ me[fn] = Ext.Function.pass(me.getRef, [
+ ref,
+ info
+ ], me);
+ }
+ me.references.push(ref.toLowerCase());
+ }
+ },
+ /**
+ * Registers one or more {@link #refs references}.
+ *
+ * @param {Object/Object[]} refs
+ */
+ addRef: function(refs) {
+ this.ref(refs);
+ },
+ getRef: function(ref, info, config) {
+ var me = this,
+ refCache = me.refCache || (me.refCache = {}),
+ cached = refCache[ref];
+ info = info || {};
+ config = config || {};
+ Ext.apply(info, config);
+ if (info.forceCreate) {
+ return Ext.ComponentManager.create(info, 'component');
+ }
+ if (!cached) {
+ if (info.selector) {
+ refCache[ref] = cached = Ext.ComponentQuery.query(info.selector)[0];
+ }
+ if (!cached && info.autoCreate) {
+ refCache[ref] = cached = Ext.ComponentManager.create(info, 'component');
+ }
+ if (cached) {
+ cached.on('destroy', function() {
+ refCache[ref] = null;
+ });
+ }
+ }
+ return cached;
+ },
+ /**
+ * Returns `true` if a {@link #refs reference} is registered.
+ *
+ * @param {String} ref The name of the ref to check for.
+ * @return {Boolean}
+ */
+ hasRef: function(ref) {
+ var references = this.references;
+ return references && Ext.Array.indexOf(references, ref.toLowerCase()) !== -1;
+ },
+ /**
+ * Returns instance of a {@link Ext.app.Controller Controller} with the given id.
+ * When controller doesn't exist yet, it's created. Note that this method depends
+ * on Application instance and will return undefined when Application is not
+ * accessible. The only exception is when this Controller instance's id is requested;
+ * in that case we always return the instance even if Application is no available.
+ *
+ * @param {String} id
+ *
+ * @return {Ext.app.Controller} controller instance or undefined.
+ */
+ getController: function(id) {
+ var app = this.getApplication();
+ if (id === this.getId()) {
+ return this;
+ }
+ return app && app.getController(id);
+ },
+ /**
+ * Returns instance of a {@link Ext.data.Store Store} with the given name.
+ * When store doesn't exist yet, it's created.
+ *
+ * @param {String} name
+ *
+ * @return {Ext.data.Store} a store instance.
+ */
+ getStore: function(name) {
+ var storeId, store;
+ storeId = (name.indexOf('@') === -1) ? name : name.split('@')[0];
+ store = Ext.StoreManager.get(storeId);
+ if (!store) {
+ name = Ext.app.Controller.getFullName(name, 'store', this.$namespace);
+ if (name) {
+ store = Ext.create(name.absoluteName, {
+ // Use id here. If the store has a configured storeId,
+ // that will take precedence
+ id: storeId
+ });
+ }
+ }
+ return store;
+ },
+ /**
+ * Returns a {@link Ext.data.Model Model} class with the given name.
+ *
+ * @param {String} modelName
+ * @return {Ext.Class} A class ultimately derived from `Ext.data.Model`.
+ */
+ getModel: function(modelName) {
+ var name = Ext.app.Controller.getFullName(modelName, 'model', this.$namespace),
+ ret = Ext.ClassManager.get(name.absoluteName);
+ if (!ret) {
+ ret = Ext.data.schema.Schema.lookupEntity(modelName);
+ }
+ return ret;
+ },
+ /**
+ * Returns instance of a {@link Ext.app.Profile Profile} with the given name.
+ *
+ * @param {String} name
+ *
+ * @return {String} a profile instance.
+ */
+ getProfile: function(name) {
+ name = Ext.app.Controller.getFullName(name, 'profile', this.$namespace);
+ return name;
+ },
+ /**
+ * Returns a View class with the given name. To create an instance of the view,
+ * you can use it like it's used by Application to create the Viewport:
+ *
+ * this.getView('Viewport').create();
+ *
+ * @param {String} view
+ *
+ * @return {Ext.Base} a view class.
+ */
+ getView: function(view) {
+ var name = Ext.app.Controller.getFullName(view, 'view', this.$namespace);
+ return name && Ext.ClassManager.get(name.absoluteName);
+ },
+ /**
+ * @method destroy
+ * @inheritdoc
+ * @param destroyRefs (private)
+ * @param fromApp (private)
+ */
+ destroy: function(destroyRefs, fromApp) {
+ var me = this,
+ app = me.application,
+ refCache, ref;
+ if (!fromApp && app) {
+ app.unregister(me);
+ }
+ me.application = null;
+ if (destroyRefs) {
+ // Possible destroy stores here too?
+ refCache = me.refCache;
+ for (ref in refCache) {
+ if (refCache.hasOwnProperty(ref)) {
+ Ext.destroy(refCache[ref]);
+ }
+ }
+ }
+ me.callParent();
+ }
+});
+
+/**
+ * Represents an Ext JS application, which is typically a single page app using a
+ * {@link Ext.container.Viewport Viewport}.
+ *
+ * An application consists of one or more Views. The behavior of a View is managed by its
+ * corresponding {@link Ext.app.ViewController ViewController} and {@link Ext.app.ViewModel
+ * ViewModel}.
+ *
+ * Global activities are coordinated by {@link Ext.app.Controller Controllers} which are
+ * ultimately instantiated by an instance of this (or a derived) class.
+ *
+ * Ext.application({
+ * name: 'MyApp',
+ *
+ * // The name of the initial view to create. With the classic toolkit this class
+ * // will gain a "viewport" plugin if it does not extend Ext.Viewport. With the
+ * // modern toolkit, the main view will be added to the Viewport.
+ * //
+ * //mainView: 'Main.view.main.Main'
+ * });
+ *
+ * This does several things. First it creates a global variable called 'MyApp' - all of
+ * your Application's classes (such as its Models, Views and Controllers) will reside under
+ * this single namespace, which drastically lowers the chances of colliding global variables.
+ *
+ * The MyApp global will also have a getApplication method to get a reference to the current
+ * application:
+ *
+ * var app = MyApp.getApplication();
+ *
+ * # Telling Application about the rest of the app
+ *
+ * Because an Ext.app.Application represents an entire app, we should tell it about the other
+ * parts of the app - namely the Models, Views and Controllers that are bundled with the
+ * application. Let's say we have a blog management app; we might have Models and Controllers for
+ * Posts and Comments, and Views for listing, adding and editing Posts and Comments. Here's how
+ * we'd tell our Application about all these things:
+ *
+ * Ext.application({
+ * name: 'Blog',
+ *
+ * models: ['Post', 'Comment'],
+ *
+ * controllers: ['Posts', 'Comments'],
+ *
+ * launch: function() {
+ * ...
+ * }
+ * });
+ *
+ * Note that we didn't actually list the Views directly in the Application itself. This is because
+ * Views are managed by Controllers, so it makes sense to keep those dependencies there.
+ * The Application will load each of the specified Controllers using the pathing conventions
+ * laid out in the [application architecture guide][1]
+ * - in this case expecting the controllers to reside in app/controller/Posts.js and
+ * app/controller/Comments.js. In turn, each Controller simply needs to list the Views it uses
+ * and they will be automatically loaded. Here's how our Posts controller like be defined:
+ *
+ * Ext.define('MyApp.controller.Posts', {
+ * extend: 'Ext.app.Controller',
+ * views: ['posts.List', 'posts.Edit'],
+ *
+ * // the rest of the Controller here
+ * });
+ *
+ * Because we told our Application about our Models and Controllers, and our Controllers about
+ * their Views, Ext JS will automatically load all of our app files for us. This means we don't
+ * have to manually add script tags into our html files whenever we add a new class, but more
+ * importantly it enables us to create a minimized build of our entire application using Sencha Cmd.
+ *
+ * # Deriving from Ext.app.Application
+ *
+ * Typically, applications do not derive directly from Ext.app.Application. Rather, the
+ * configuration passed to `Ext.application` mimics what you might do in a derived class.
+ * In some cases, however, it can be desirable to share logic by using a derived class
+ * from `Ext.app.Application`.
+ *
+ * Derivation works as you would expect, but using the derived class should still be the
+ * job of the `Ext.application` method.
+ *
+ * Ext.define('MyApp.Application', {
+ * extend: 'Ext.app.Application',
+ * name: 'MyApp',
+ * ...
+ * });
+ *
+ * Ext.application('MyApp.Application');
+ *
+ * For more information about writing Ext JS applications, please see
+ * the [application architecture guide][1].
+ * [1]: ../guides/application_architecture/application_architecture.html
+ */
+Ext.define('Ext.app.Application', {
+ extend: Ext.app.Controller,
+ isApplication: true,
+ /**
+ * @cfg {String} extend A class name to use with the `Ext.application` call. The class must
+ * also extend {@link Ext.app.Application}.
+ *
+ * Ext.define('MyApp.Application', {
+ * extend: 'Ext.app.Application',
+ *
+ * launch: function() {
+ * Ext.direct.Manager.addProvider(Ext.REMOTING_API);
+ * }
+ * });
+ *
+ * Ext.application({
+ * extend: 'MyApp.Application'
+ * });
+ */
+ /**
+ * @cfg {String/String[]} controllers
+ * Names of {@link Ext.app.Controller controllers} that the app uses. By default,
+ * the framework will look for the controllers in the "controller" folder within the
+ * {@link #appFolder}. Controller classes should be named using the syntax of
+ * "{appName}.controller.{ClassName}" with additional sub-folders under the
+ * "controller" folder specified within the class name following "controller.".
+ *
+ * // by default, the following controller class would be located at:
+ * // app/controller/Main.js
+ * controllers: '.Main' // or 'MyApp.controller.Main'
+ *
+ * // while the following would be located at:
+ * // app/controller/customer/Main.js
+ * controllers: 'customer.Main' // or 'MyApp.controller.customer.Main'
+ *
+ * **Note:** If the controller has a different namespace than that of the
+ * application you will need to specify the full class name as well as define a path
+ * in the {@link Ext.Loader#cfg-paths Loader's paths} config or
+ * {@link Ext.Loader#method-setPath setPath} method.
+ */
+ /**
+ * @cfg {Object} scope
+ * The scope to execute the {@link #launch} function in. Defaults to the Application instance.
+ */
+ scope: undefined,
+ /**
+ * @cfg {String/String[]} [namespaces]
+ *
+ * The list of namespace prefixes used in the application to resolve dependencies
+ * like Views and Stores:
+ *
+ * Ext.application({
+ * name: 'MyApp',
+ *
+ * namespaces: ['Common.code'],
+ *
+ * controllers: [ 'Common.code.controller.Foo', 'Bar' ]
+ * });
+ *
+ * Ext.define('Common.code.controller.Foo', {
+ * extend: 'Ext.app.Controller',
+ *
+ * models: ['Foo'], // Loads Common.code.model.Foo
+ * views: ['Bar'] // Loads Common.code.view.Bar
+ * });
+ *
+ * Ext.define('MyApp.controller.Bar', {
+ * extend: 'Ext.app.Controller',
+ *
+ * models: ['Foo'], // Loads MyApp.model.Foo
+ * views: ['Bar'] // Loads MyApp.view.Bar
+ * });
+ *
+ * You don't need to include main namespace (MyApp), it will be added to the list
+ * automatically.
+ */
+ namespaces: [],
+ /**
+ * @cfg {Object} paths
+ * Additional load paths to add to Ext.Loader.
+ * See {@link Ext.Loader#paths} config for more details.
+ */
+ paths: null,
+ /**
+ * @cfg {String} [appFolder="app"]
+ * The path to the directory which contains all application's classes.
+ * This path will be registered via {@link Ext.Loader#setPath} for the namespace specified
+ * in the {@link #name name} config.
+ */
+ // NOTE - this config has to be processed by Ext.application
+ config: {
+ /**
+ * @cfg {String} name
+ * The name of your application. This will also be the namespace for your views, controllers
+ * models and stores. Don't use spaces or special characters in the name. **Application name
+ * is mandatory**.
+ */
+ name: '',
+ /**
+ * @cfg {String} appProperty
+ * The name of a property to be assigned to the main namespace to gain a reference to
+ * this application. Can be set to an empty value to prevent the reference from
+ * being created
+ *
+ * Ext.application({
+ * name: 'MyApp',
+ * appProperty: 'myProp',
+ *
+ * launch: function() {
+ * console.log(MyApp.myProp === this);
+ * }
+ * });
+ */
+ appProperty: 'app',
+ // @cmd-auto-dependency { aliasPrefix: "profile.", mvc: true, blame: "all" }
+ /**
+ * @cfg {String/String[]} profiles
+ * Names of the profiles that the app uses.
+ */
+ profiles: [],
+ /**
+ * @cfg {Ext.app.Profile}
+ */
+ currentProfile: null,
+ // @cmd-auto-dependency {aliasPrefix: "view.", mvc: true, blame: "all"}
+ /**
+ * @cfg {String/Object/Ext.Component} mainView
+ * The application class to be used as the main viewport view for the
+ * application. The view will be configured with the
+ * {@link Ext.plugin.Viewport viewport plugin} to ensure the view takes up all
+ * available space in the browser viewport. The main view will be created after
+ * the application's {@link #init} method is called and before the
+ * {@link #launch} method. The main view should be an application class type and
+ * not a class from the framework.
+ *
+ * The main view value may be:
+ * - string representing the full class name of the main view or the partial class name
+ * following "AppName.view." (provided your main view class follows that convention).
+ * - config object for the main view
+ * - main view class instance
+ *
+ * Ext.define('MyApp.view.main.Main', {
+ * extend: 'Ext.panel.Panel',
+ * xtype: 'mainview',
+ * title: 'Main Viewport View'
+ * });
+ *
+ * Ext.application({
+ * name : 'MyApp',
+ *
+ * mainView: 'MyApp.view.main.Main'
+ * // mainView: 'main.Main'
+ * // mainView: new MyApp.view.main.Main()
+ * // mainView: { xtype: 'mainview' }
+ * });
+ *
+ * **Note:** You may also call {@link #setMainView} at runtime if you require
+ * logic within the application's {@link #launch} method to be processed prior to
+ * the creation of the main view.
+ */
+ mainView: {
+ $value: null,
+ lazy: true
+ },
+ /**
+ * @cfg {String} [defaultToken=null] The default token to be used at application launch
+ * if one is not present. Often this is set to something like `'home'`.
+ */
+ defaultToken: null,
+ /**
+ * @cfg {String} glyphFontFamily
+ * The glyphFontFamily to use for this application. Used as the default font-family
+ * for all components that support a `glyph` config.
+ */
+ glyphFontFamily: null,
+ // Docs will go in subclasses
+ quickTips: true,
+ /**
+ * @cfg {Object} router
+ * A configuration object to apply onto the {@link Ext.route.Router Router}.
+ * @since 6.5.0
+ */
+ router: null
+ },
+ onClassExtended: function(cls, data, hooks) {
+ var Controller = Ext.app.Controller,
+ proto = cls.prototype,
+ requires = [],
+ onBeforeClassCreated, namespace;
+ // Ordinary inheritance does not work here so we collect
+ // necessary data from current class data and its superclass
+ namespace = data.name || cls.superclass.name;
+ if (namespace) {
+ data.$namespace = namespace;
+ Ext.app.addNamespaces(namespace);
+ }
+ if (data.namespaces) {
+ Ext.app.addNamespaces(data.namespaces);
+ }
+ if (data['paths processed']) {
+ delete data['paths processed'];
+ } else {
+ Ext.app.setupPaths(namespace, ('appFolder' in data) ? data.appFolder : cls.superclass.appFolder, data.paths);
+ }
+ // Require all profiles
+ Controller.processDependencies(proto, requires, namespace, 'profile', data.profiles);
+ // This hook is used in the classic toolkit to process other configs that need to
+ // require classes (like tooltips and viewport plugin).
+ proto.getDependencies(cls, data, requires);
+ // Any "requires" also have to be processed before we fire up the App instance.
+ if (requires.length) {
+ onBeforeClassCreated = hooks.onBeforeCreated;
+ hooks.onBeforeCreated = function(cls, data) {
+ var args = Ext.Array.clone(arguments);
+ // This hook is to allow unit tests to come in and control the
+ // requires so we don't have to get into the internals of the Loader.
+ // Not intended to be used for any other purpose.
+ if (data.__handleRequires) {
+ data.__handleRequires.call(this, requires, Ext.bind(function() {
+ return onBeforeClassCreated.apply(this, args);
+ }, this));
+ return;
+ }
+ Ext.require(requires, function() {
+ return onBeforeClassCreated.apply(this, args);
+ });
+ };
+ }
+ },
+ getDependencies: Ext.emptyFn,
+ /**
+ * Creates new Application.
+ * @param {Object} [config] Config object.
+ */
+ constructor: function(config) {
+ var me = this;
+ Ext.route.Router.application = me;
+ me.callParent([
+ config
+ ]);
+ if (Ext.isEmpty(me.getName())) {
+ Ext.raise("[Ext.app.Application] Name property is required");
+ }
+ me.doInit(me);
+ Ext.on('appupdate', me.onAppUpdate, me, {
+ single: true
+ });
+ Ext.Loader.setConfig({
+ enabled: true
+ });
+ this.onProfilesReady();
+ },
+ applyId: function(id) {
+ return id || this.$className;
+ },
+ updateRouter: function(cfg) {
+ if (cfg) {
+ Ext.route.Router.setConfig(cfg);
+ }
+ },
+ /**
+ * @method
+ * @template
+ * Called automatically when an update to either the Application Cache or the Local Storage
+ * Cache is detected.
+ * This is mainly used during production builds.
+ * @param {Object} [updateInfo] updateInfo Update information object contains properties for
+ * checking which cache triggered the update
+ */
+ onAppUpdate: Ext.emptyFn,
+ onProfilesReady: function() {
+ var me = this,
+ profiles = me.getProfiles(),
+ length = profiles.length,
+ current, i, instance;
+ for (i = 0; i < length; i++) {
+ instance = Ext.create(profiles[i], {
+ application: me
+ });
+ if (instance.isActive() && !current) {
+ current = instance;
+ me.setCurrentProfile(current);
+ }
+ }
+ if (current) {
+ current.init();
+ }
+ me.initControllers();
+ me.onBeforeLaunch();
+ me.finishInitControllers();
+ },
+ doInit: function(app) {
+ this.initNamespace(app);
+ this.callParent([
+ app
+ ]);
+ },
+ initNamespace: function(me) {
+ var appProperty = me.getAppProperty(),
+ ns = Ext.namespace(me.getName());
+ if (ns) {
+ ns.getApplication = function() {
+ return me;
+ };
+ if (appProperty) {
+ if (!ns[appProperty]) {
+ ns[appProperty] = me;
+ } else if (ns[appProperty] !== me) {
+ Ext.log.warn('An existing reference is being overwritten for ' + name + '.' + appProperty + '. See the appProperty config.');
+ }
+ }
+ }
+ },
+ initControllers: function() {
+ var me = this,
+ controllers = Ext.Array.from(me.controllers),
+ profile = me.getCurrentProfile(),
+ i, ln;
+ me.controllers = new Ext.util.MixedCollection();
+ for (i = 0 , ln = controllers.length; i < ln; i++) {
+ me.getController(controllers[i]);
+ }
+ // Also launch controllers for the active profile (if we have one)
+ //
+ if (profile) {
+ controllers = profile.getControllers();
+ for (i = 0 , ln = controllers.length; i < ln; i++) {
+ me.getController(controllers[i]);
+ }
+ }
+ },
+ finishInitControllers: function() {
+ var me = this,
+ controllers, i, l;
+ controllers = me.controllers.getRange();
+ for (i = 0 , l = controllers.length; i < l; i++) {
+ controllers[i].finishInit(me);
+ }
+ },
+ /**
+ * @method
+ * @template
+ * Called automatically when the page has completely loaded. This is an empty function that
+ * should be overridden by each application that needs to take action on page load.
+ * @param {String} profile The detected application profile
+ * @return {Boolean} By default, the Application will dispatch to the configured startup
+ * controller and action immediately after running the launch function. Return false
+ * to prevent this behavior.
+ */
+ launch: Ext.emptyFn,
+ /**
+ * @private
+ */
+ onBeforeLaunch: function() {
+ var me = this,
+ History = Ext.util.History,
+ defaultToken = me.getDefaultToken(),
+ currentProfile = me.getCurrentProfile(),
+ controllers, c, cLen, controller, token;
+ me.initMainView();
+ if (currentProfile) {
+ currentProfile.launch();
+ }
+ me.launch.call(me.scope || me);
+ me.launched = true;
+ me.fireEvent('launch', me);
+ controllers = me.controllers.items;
+ cLen = controllers.length;
+ for (c = 0; c < cLen; c++) {
+ controller = controllers[c];
+ controller.onLaunch(me);
+ }
+ if (!History.ready) {
+ History.init();
+ }
+ token = History.getToken();
+ if (token || token === defaultToken) {
+ Ext.route.Router.onStateChange(token);
+ } else if (defaultToken) {
+ History.replace(defaultToken);
+ }
+ // Microloader has detected an Application Cache or LocalStorage Cache update, inform
+ // everyone that may have added listeners late.
+ if (Ext.Microloader && Ext.Microloader.appUpdate && Ext.Microloader.appUpdate.updated) {
+ Ext.Microloader.fireAppUpdate();
+ }
+ // After launch we may as well cleanup the namespace cache
+ if (!me.cnsTimer) {
+ me.cnsTimer = Ext.defer(Ext.ClassManager.clearNamespaceCache, 2000, Ext.ClassManager);
+ }
+ },
+ getModuleClassName: function(name, kind) {
+ return Ext.app.Controller.getFullName(name, kind, this.getName()).absoluteName;
+ },
+ initMainView: function() {
+ var me = this,
+ currentProfile = me.getCurrentProfile(),
+ mainView;
+ if (currentProfile) {
+ mainView = currentProfile.getMainView();
+ }
+ if (mainView) {
+ me.setMainView(mainView);
+ } else {
+ // since mainView is a lazy config we have to call the getter to initialize it
+ me.getMainView();
+ }
+ },
+ applyMainView: function(value) {
+ var view = this.getView(value);
+ // Ensure the full component stack is available immediately.
+ return view.create({
+ $initParent: this.viewport
+ });
+ },
+ /**
+ * Create an instance of a controller by name.
+ * @param {String} name The name of the controller. For a controller with the
+ * full class name `MyApp.controller.Foo`, the name parameter should be `Foo`.
+ * If the controller already exists, it will be returned.
+ *
+ * @return {Ext.app.Controller} controller
+ */
+ createController: function(name) {
+ return this.getController(name);
+ },
+ /**
+ * Destroys a controller, any listeners are unbound.
+ * @param {String/Ext.app.Controller} controller The controller
+ */
+ destroyController: function(controller) {
+ if (typeof controller === 'string') {
+ controller = this.getController(controller, true);
+ }
+ Ext.destroy(controller);
+ },
+ /**
+ * Get an application's controller based on name or id. Generally, the controller id will be
+ * the same as the name unless otherwise specified.
+ * @param {String} name The name or id of the controller you are trying to retrieve
+ * @param {Boolean} [preventCreate] (private)
+ */
+ getController: function(name, preventCreate) {
+ var me = this,
+ controllers = me.controllers,
+ className, controller, len, i, c, all;
+ // First check with the passed value if we have an explicit id
+ controller = controllers.get(name);
+ // In a majority of cases, the controller id will be the same as the name.
+ // However, when a controller is manually given an id, it will be keyed
+ // in the collection that way. So if we don't find it, we attempt to loop
+ // over the existing controllers and find it by classname
+ if (!controller) {
+ all = controllers.items;
+ for (i = 0 , len = all.length; i < len; ++i) {
+ c = all[i];
+ className = c.getModuleClassName();
+ if (className && className === name) {
+ controller = c;
+ break;
+ }
+ }
+ }
+ if (!controller && !preventCreate) {
+ className = me.getModuleClassName(name, 'controller');
+ controller = Ext.create(className, {
+ application: me,
+ moduleClassName: className
+ });
+ controllers.add(controller);
+ if (me._initialized) {
+ controller.doInit(me);
+ }
+ }
+ return controller;
+ },
+ /**
+ * Unregister a controller from the application.
+ * @private
+ * @param {Ext.app.Controller} controller The controller to unregister
+ */
+ unregister: function(controller) {
+ this.controllers.remove(controller);
+ },
+ getApplication: function() {
+ return this;
+ },
+ destroy: function(destroyRefs) {
+ var me = this,
+ controllers = me.controllers,
+ ns = Ext.namespace(me.getName()),
+ appProp = me.getAppProperty();
+ Ext.undefer(me.cnsTimer);
+ Ext.un('appupdate', me.onAppUpdate, me);
+ Ext.destroy(me.viewport);
+ if (controllers) {
+ controllers.each(function(controller) {
+ controller.destroy(destroyRefs, true);
+ });
+ }
+ me.controllers = null;
+ me.callParent([
+ destroyRefs,
+ true
+ ]);
+ // Clean up any app reference
+ if (ns && ns[appProp] === me) {
+ delete ns[appProp];
+ }
+ if (Ext.route.Router.application === me) {
+ Ext.route.Router.application = null;
+ }
+ if (Ext.app.Application.instance === me) {
+ Ext.app.Application.instance = null;
+ }
+ },
+ updateGlyphFontFamily: function(fontFamily) {
+ Ext.setGlyphFontFamily(fontFamily);
+ },
+ /**
+ * As a convenience developers can locally qualify profile names (e.g. 'MyProfile' vs
+ * 'MyApp.profile.MyProfile'). This just makes sure everything ends up fully qualified.
+ * @private
+ */
+ applyProfiles: function(profiles) {
+ var me = this;
+ return Ext.Array.map(profiles, function(profile) {
+ return me.getModuleClassName(profile, "profile");
+ });
+ }
+}, function() {
+ /**
+ * @member Ext
+ * @method getApplication
+ * @return {Ext.app.Application}
+ */
+ Ext.getApplication = function() {
+ return Ext.app.Application.instance;
+ };
+});
+
+// This is an override because it must be loaded very early, possibly before Ext.app.Application
+// in dev mode so that Ext.application() can be called.
+// Being an override also ensures that it is only included in a built app if Ext.app.Application
+// is present.
+//
+// @override Ext.app.Application
+/**
+ * @method application
+ * @member Ext
+ * Loads Ext.app.Application class and starts it up with given configuration after the
+ * page is ready.
+ *
+ * See `Ext.app.Application` for details.
+ *
+ * @param {Object/String} config Application config object or name of a class derived
+ * from Ext.app.Application.
+ */
+Ext.application = function(config) {
+ var createApp = function(App) {
+ // This won't be called until App class has been created.
+ Ext.onReady(function() {
+ var Viewport = Ext.viewport;
+ // eslint-disable-next-line dot-notation
+ Viewport = Viewport && Viewport['Viewport'];
+ if (Viewport && Viewport.setup) {
+ Viewport.setup(App.prototype.config.viewport);
+ }
+ Ext.app.Application.instance = new App();
+ });
+ };
+ if (typeof config === "string") {
+ Ext.require(config, function() {
+ createApp(Ext.ClassManager.get(config));
+ });
+ } else {
+ config = Ext.apply({
+ extend: 'Ext.app.Application'
+ }, // can be replaced by config!
+ config);
+ // We have to process "paths" before creating Application class,
+ // or `requires` won't work.
+ Ext.app.setupPaths(config.name, config.appFolder, config.paths);
+ config['paths processed'] = true;
+ // Let Ext.define do the hard work but don't assign a class name.
+ Ext.define(config.name + ".$application", config, function() {
+ createApp(this);
+ });
+ }
+};
+
+/**
+ * @private
+ */
+Ext.define('Ext.util.ItemCollection', {
+ extend: Ext.util.MixedCollection,
+ alternateClassName: 'Ext.ItemCollection',
+ getKey: function(item) {
+ return item.getItemId && item.getItemId();
+ },
+ has: function(item) {
+ return this.map.hasOwnProperty(item.getId());
+ }
+});
+
+/**
+ * @private
+ * A mixin for providing query related methods for {@link Ext.ComponentQuery} for classes that
+ * implement getRefItems.
+ */
+Ext.define('Ext.mixin.Queryable', {
+ mixinId: 'queryable',
+ isQueryable: true,
+ /**
+ * Retrieves all descendant components which match the passed selector.
+ * Executes an Ext.ComponentQuery.query using this container as its root.
+ * @param {String} [selector] Selector complying to an Ext.ComponentQuery selector.
+ * If no selector is specified all items will be returned.
+ * @return {Ext.Component[]} Components which matched the selector
+ */
+ query: function(selector) {
+ selector = selector || '*';
+ return Ext.ComponentQuery.query(selector, this.getQueryRoot());
+ },
+ /**
+ * Retrieves all descendant components which match the passed function.
+ * The function should return false for components that are to be
+ * excluded from the selection.
+ * @param {Function} fn The matcher function. It will be called with a single argument,
+ * the component being tested.
+ * @param {Object} [scope] The scope in which to run the function. If not specified,
+ * it will default to the active component.
+ * @return {Ext.Component[]} Components matched by the passed function
+ */
+ queryBy: function(fn, scope) {
+ var out = [],
+ items = this.getQueryRoot().getRefItems(true),
+ i = 0,
+ len = items.length,
+ item;
+ for (; i < len; ++i) {
+ item = items[i];
+ if (fn.call(scope || item, item) !== false) {
+ out.push(item);
+ }
+ }
+ return out;
+ },
+ /**
+ * Finds a component at any level under this container matching the id/itemId.
+ * This is a shorthand for calling ct.down('#' + id);
+ * @param {String} id The id to find
+ * @return {Ext.Component} The matching id, null if not found
+ */
+ queryById: function(id) {
+ return this.down(Ext.makeIdSelector(id));
+ },
+ /**
+ * Retrieves the first direct child of this container which matches the passed selector
+ * or component. The passed in selector must comply with an Ext.ComponentQuery selector,
+ * or it can be an actual Ext.Component.
+ * @param {String/Ext.Component} [selector] An Ext.ComponentQuery selector. If no selector is
+ * specified, the first child will be returned.
+ * @return {Ext.Component} The matching child Ext.Component (or `null` if no match was found).
+ */
+ child: function(selector) {
+ var children = this.getQueryRoot().getRefItems();
+ if (selector && selector.isComponent) {
+ return this.matchById(children, selector.getItemId());
+ }
+ // Filter children array to only matches.
+ if (selector) {
+ children = Ext.ComponentQuery.query(selector, children);
+ }
+ // Return first match
+ if (children.length) {
+ return children[0];
+ }
+ return null;
+ },
+ /**
+ * Retrieves the first descendant of this container which matches the passed selector.
+ * The passed in selector must comply with an Ext.ComponentQuery selector, or it can be
+ * an actual Ext.Component.
+ * @param {String/Ext.Component} [selector] An Ext.ComponentQuery selector or Ext.Component.
+ * If no selector is
+ * specified, the first child will be returned.
+ * @return {Ext.Component} The matching descendant Ext.Component (or `null` if no match
+ * was found).
+ */
+ down: function(selector) {
+ if (selector && selector.isComponent) {
+ return this.matchById(this.getRefItems(true), selector.getItemId());
+ }
+ selector = selector || '';
+ return this.query(selector)[0] || null;
+ },
+ /**
+ * Traverses the tree rooted at this node in pre-order mode, calling the passed function
+ * on the nodes at each level. That is the function is called upon each node **before**
+ * being called on its children).
+ *
+ * This method is used at each level down the cascade. Currently
+ * {@link Ext.Component Component}s and {@link Ext.data.TreeModel TreeModel}s are queryable.
+ *
+ * If you have tree-structured data, you can make your nodes queryable, and use ComponentQuery
+ * on them.
+ *
+ * @param {Object} selector A ComponentQuery selector used to filter candidate nodes before
+ * calling the function. An empty string matches any node.
+ * @param {Function} fn The function to call. Return `false` to abort the traverse.
+ * @param {Object} fn.node The node being visited.
+ * @param {Object} [scope] The context (`this` reference) in which the function is executed.
+ * @param {Array} [extraArgs] A set of arguments to be appended to the function's argument list
+ * to pass down extra data known to the caller **after** the node being visited.
+ */
+ visitPreOrder: function(selector, fn, scope, extraArgs) {
+ Ext.ComponentQuery._visit(true, selector, this.getQueryRoot(), fn, scope, extraArgs);
+ },
+ /**
+ * Traverses the tree rooted at this node in post-order mode, calling the passed function
+ * on the nodes at each level. That is the function is called upon each node **after** being
+ * called on its children).
+ *
+ * This method is used at each level down the cascade. Currently
+ * {@link Ext.Component Component}s and {@link Ext.data.TreeModel TreeModel}s are queryable.
+ *
+ * If you have tree-structured data, you can make your nodes queryable, and use ComponentQuery
+ * on them.
+ *
+ * @param {Object} selector A ComponentQuery selector used to filter candidate nodes before
+ * calling the function. An empty string matches any node.
+ * @param {Function} fn The function to call. Return `false` to abort the traverse.
+ * @param {Object} fn.node The node being visited.
+ * @param {Object} [scope] The context (`this` reference) in which the function is executed.
+ * @param {Array} [extraArgs] A set of arguments to be appended to the function's argument list
+ * to pass down extra data known to the caller **after** the node being visited.
+ */
+ visitPostOrder: function(selector, fn, scope, extraArgs) {
+ Ext.ComponentQuery._visit(false, selector, this.getQueryRoot(), fn, scope, extraArgs);
+ },
+ getRefItems: function() {
+ return [];
+ },
+ getQueryRoot: function() {
+ return this;
+ },
+ privates: {
+ matchById: function(items, id) {
+ var len = items.length,
+ i, item;
+ for (i = 0; i < len; ++i) {
+ item = items[i];
+ if (item.getItemId() === id) {
+ return item;
+ }
+ }
+ return null;
+ }
+ }
+});
+
+/**
+ * @private
+ * Common methods for both classic & modern containers
+ */
+Ext.define('Ext.mixin.Container', {
+ extend: Ext.Mixin,
+ mixinConfig: {
+ id: 'container'
+ },
+ /**
+ * @property {Boolean} isContainer
+ * `true` in this class to identify an object as an instantiated Container, or subclass thereof.
+ */
+ isContainer: true,
+ /**
+ * @cfg {Boolean} nameHolder
+ * When `true` child components are tracked by their `name` property and can be
+ * retrieved using the `lookupName` method.
+ */
+ nameHolder: false,
+ /**
+ * @cfg {Boolean} referenceHolder
+ * If `true`, this container will be marked as being a point in the hierarchy where
+ * references to items with a specified `reference` config will be held. The container
+ * will automatically become a referenceHolder if a {@link #controller} is specified.
+ *
+ * See the introductory docs for {@link Ext.container.Container} for more information
+ * about references & reference holders.
+ */
+ referenceHolder: false,
+ /**
+ * Returns an object holding the descendants of this container keyed by their
+ * `name`. This object should not be held past the scope of the function calling this
+ * method. It will not be valid if items are added or removed from this or any
+ * sub-container.
+ *
+ * The intended usage is shown here (assume there are 3 components with names of
+ * "foo", "bar" and "baz" at some level below this container):
+ *
+ * onClick: function () {
+ * var items = this.getNamedItems();
+ *
+ * // using "items" we can access any descendant by its "name"
+ *
+ * items.foo.getValue() + items.bar.getValue() + items.baz.getValue();
+ * }
+ *
+ * If `this` component has a `name` assigned to it, it is **not** included in this
+ * object. That name is understood to belong to the ancestor container configured
+ * as the `nameHolder`.
+ *
+ * @return {Object} An object with each named child. This will be `null` if this
+ * container has no descendants with a `name` specified.
+ * @since 6.5.0
+ */
+ getNamedItems: function() {
+ if (Ext.referencesDirty) {
+ Ext.fixReferences();
+ }
+ return this.nameRefs || null;
+ },
+ /**
+ * Returns an object holding the descendants of this view keyed by their
+ * `{@link Ext.Component#cfg-reference reference}`. This object should not be held
+ * past the scope of the function calling this method. It will not be valid if items
+ * are added or removed from this or any sub-container.
+ *
+ * The intended usage is shown here (assume there are 3 components with reference
+ * values of "foo", "bar" and "baz" at some level below this container):
+ *
+ * onClick: function () {
+ * var refs = this.getReferences();
+ *
+ * // using "refs" we can access any descendant by its "reference"
+ *
+ * refs.foo.getValue() + refs.bar.getValue() + refs.baz.getValue();
+ * }
+ *
+ * If `this` component has a `{@link Ext.Component#cfg-reference reference}` assigned
+ * to it, that is **not** included in this object. That reference is understood to
+ * belong to the ancestor container configured as the `referenceHolder`.
+ *
+ * @return {Object} An object with each child reference. This will be `null` if this
+ * container has no descendants with a `{@link Ext.Component#cfg-reference reference}`
+ * specified.
+ * @since 5.0.0
+ */
+ getReferences: function() {
+ if (Ext.referencesDirty) {
+ Ext.fixReferences();
+ }
+ return this.refs || null;
+ },
+ /**
+ * Gets a reference to the component with the specified
+ * {@link Ext.Component#cfg-reference reference} value.
+ *
+ * The method is a short-hand for the {@link #lookupReference} method.
+ *
+ * @param {String} ref The value of the `reference` to lookup.
+ * @return {Ext.Component} The referenced component or `null` if it is not found.
+ * @since 6.0.1
+ */
+ lookup: function(ref) {
+ var refs = this.getReferences();
+ return (refs && refs[ref]) || null;
+ },
+ /**
+ * Gets a reference to the component with the specified `name` property.
+ *
+ * @param {String} name The name of the descendant to lookup.
+ * @return {Ext.Component} The component or `null` if it is not found.
+ * @since 6.5.0
+ */
+ lookupName: function(name) {
+ var items = this.getNamedItems();
+ return (items && items[name]) || null;
+ },
+ /**
+ * Gets a reference to the component with the specified {@link #reference} value.
+ *
+ * The {@link #lookup} method is a short-hand version of this method.
+ *
+ * @param {String} ref The name of the reference to lookup.
+ * @return {Ext.Component} The referenced component or `null` if it is not found.
+ * @since 5.0
+ */
+ lookupReference: function(ref) {
+ return this.lookup(ref);
+ },
+ privates: {
+ /**
+ * Sets up a component name reference.
+ * @param {Ext.Component} component The component to reference.
+ * @private
+ */
+ attachNameRef: function(component) {
+ var me = this,
+ key = component.name || component._name,
+ entry, nameRefs;
+ // Cleaning all this up later anyway
+ if (key && !me.destroying && !me.destroyed) {
+ nameRefs = me.nameRefs || (me.nameRefs = {});
+ entry = nameRefs[key];
+ if (!entry) {
+ entry = component.shareableName ? [
+ component
+ ] : component;
+ } else if (!entry.isInstance) {
+ // Else an existing entry is either a component (which will have false
+ // for shareableName) or an array (all elements of which have true
+ // for their shareableName).
+ entry.push(component);
+ } else {
+ Ext.raise('Duplicate name: "' + key + '" on ' + me.id + ' between ' + entry.id + ' and ' + component.id);
+ }
+ nameRefs[key] = entry;
+ }
+ },
+ /**
+ * Sets up a component reference.
+ * @param {Ext.Component} component The component to reference.
+ * @private
+ */
+ attachReference: function(component) {
+ var me = this,
+ key, refs;
+ // Cleaning all this up later anyway
+ if (!me.destroying && !me.destroyed) {
+ refs = me.refs || (me.refs = {});
+ key = component.referenceKey;
+ if (refs[key] && refs[key] !== component) {
+ Ext.log.warn('Duplicate reference: "' + key + '" on ' + me.id);
+ }
+ refs[key] = component;
+ }
+ },
+ containerOnAdded: function(component, instanced) {
+ // We have been added to a container, we may have child references
+ // or be a reference ourselves. At this point we have no way of knowing if
+ // our references are correct, so trigger a fix.
+ Ext.ComponentManager.markReferencesDirty();
+ },
+ containerOnRemoved: function(destroying) {
+ // If we're destroying this will get cleaned up anyway
+ if (!destroying) {
+ // Clear any references here, they will be reset after the
+ // next call to lookupReference after being marked dirty.
+ // It's easier to wipe & re-establish them than attempt to
+ // track what changed and prune the collection
+ Ext.ComponentManager.markReferencesDirty();
+ }
+ },
+ initContainerInheritedState: function(inheritedState, inheritedStateInner) {
+ var me = this,
+ controller = me.getController(),
+ session = me.getSession(),
+ // Don't instantiate it here, we just want to know whether we
+ // were configured with a VM
+ viewModel = me.getConfig('viewModel', true),
+ reference = me.reference,
+ referenceHolder = me.referenceHolder;
+ if (me.nameHolder) {
+ inheritedState.nameHolder = me;
+ }
+ if (controller) {
+ inheritedState.referenceHolder = controller;
+ referenceHolder = true;
+ } else if (referenceHolder) {
+ inheritedState.referenceHolder = me;
+ }
+ if (referenceHolder) {
+ inheritedState.referencePath = '';
+ } else if (reference && me.isParentReference) {
+ inheritedState.referencePath = me.referenceKey + '.';
+ }
+ if (session) {
+ inheritedState.session = session;
+ }
+ if (viewModel) {
+ inheritedState.viewModelPath = '';
+ } else if (reference && me.isParentReference) {
+ inheritedState.viewModelPath = me.viewModelKey + '.';
+ }
+ },
+ setupReference: function(reference) {
+ var len;
+ if (reference && reference.charAt(len = reference.length - 1) === '>') {
+ this.isParentReference = true;
+ reference = reference.substring(0, len);
+ }
+ if (reference && !Ext.validIdRe.test(reference)) {
+ Ext.Error.raise('Invalid reference "' + reference + '" for ' + this.getId() + ' - not a valid identifier');
+ }
+ return reference;
+ }
+ }
+});
+
+/**
+ * Handles mapping key events to handling functions for an element or a Component.
+ * One KeyMap can be used for multiple actions.
+ *
+ * A KeyMap must be configured with a {@link #target} as an event source which may be
+ * an Element or a Component.
+ *
+ * If the target is an element, then the `keydown` event will trigger the invocation
+ * of {@link #binding}s.
+ *
+ * It is possible to configure the KeyMap with a custom {@link #eventName} to listen for.
+ * This may be useful when the {@link #target} is a Component.
+ *
+ * The KeyMap's event handling requires that the first parameter passed is a key event.
+ * So if the Component's event signature is different, specify a {@link #processEvent}
+ * configuration which accepts the event's parameters and returns a key event.
+ *
+ * Functions specified in {@link #binding}s are called with this signature:
+ * `(String key, Ext.event.Event e)` (if the match is a multi-key combination
+ * the callback will still be called only once). A KeyMap can also handle a string
+ * representation of keys. By default KeyMap starts enabled.
+ *
+ * Usage:
+ *
+ * // map one key by key code
+ * var map = new Ext.util.KeyMap({
+ * target: "my-element",
+ * key: 13, // or Ext.event.Event.ENTER
+ * handler: myHandler,
+ * scope: myObject
+ * });
+ *
+ * // map multiple keys to one action by string
+ * var map = new Ext.util.KeyMap({
+ * target: "my-element",
+ * key: "a\r\n\t",
+ * handler: myHandler,
+ * scope: myObject
+ * });
+ *
+ * // map multiple keys to multiple actions by strings and array of codes
+ * var map = new Ext.util.KeyMap({
+ * target: "my-element",
+ * binding: [{
+ * key: [10, 13],
+ * handler: function() {
+ * alert("Return was pressed");
+ * }
+ * }, {
+ * key: "abc",
+ * handler: function() {
+ * alert('a, b or c was pressed');
+ * }
+ * }, {
+ * key: "\t",
+ * ctrl: true,
+ * shift: true,
+ * handler: function() {
+ * alert('Control + shift + tab was pressed.');
+ * }
+ * }]
+ * });
+ *
+ * KeyMaps can also bind to Components and process key-based events fired by Components.
+ *
+ * To bind to a Component, include the Component event name to listen for, and a `processEvent`
+ * implementation which returns the key event for further processing by the KeyMap:
+ *
+ * var map = new Ext.util.KeyMap({
+ * target: myGridView,
+ * eventName: 'itemkeydown',
+ * processEvent: function(view, record, node, index, event) {
+ * // Load the event with the extra information needed by the mappings
+ * event.view = view;
+ * event.store = view.getStore();
+ * event.record = record;
+ * event.index = index;
+ * return event;
+ * },
+ * binding: {
+ * key: Ext.event.Event.DELETE,
+ * handler: function(keyCode, e) {
+ * e.store.remove(e.record);
+ *
+ * // Attempt to select the record that's now in its place
+ * e.view.getSelectionModel().select(e.index);
+ * }
+ * }
+ * });
+ */
+Ext.define('Ext.util.KeyMap', {
+ alternateClassName: 'Ext.KeyMap',
+ /**
+ * @property {Ext.event.Event} lastKeyEvent
+ * The last key event that this KeyMap handled.
+ */
+ /**
+ * @cfg {Ext.Component/Ext.dom.Element/HTMLElement/String} target
+ * The object on which to listen for the event specified by the {@link #eventName}
+ * config option.
+ */
+ /**
+ * @cfg {Object/Object[][]} binding
+ * Either a single object describing a handling function for s specified key (or set of keys),
+ * or an array of such objects.
+ * @cfg {String/String[]} binding.key A single keycode or an array of keycodes to handle, or
+ * a RegExp which specifies characters to handle, eg `/[a-z]/`.
+ * @cfg {Boolean} binding.shift `true` to handle key only when shift is pressed, `false`
+ * to handle the key only when shift is not pressed (defaults to undefined)
+ * @cfg {Boolean} binding.ctrl `true` to handle key only when ctrl is pressed, `false`
+ * to handle the key only when ctrl is not pressed (defaults to undefined)
+ * @cfg {Boolean} binding.alt `true` to handle key only when alt is pressed, `false`
+ * to handle the key only when alt is not pressed (defaults to undefined)
+ * @cfg {Function} binding.handler The function to call when KeyMap finds the expected
+ * key combination
+ * @cfg {Function} binding.fn Alias of handler (for backwards-compatibility)
+ * @cfg {Object} binding.scope The scope (`this` context) in which the handler function
+ * is executed.
+ * @cfg {String} binding.defaultEventAction A default action to apply to the event
+ * *when the handler returns `true`*. Possible values are: stopEvent, stopPropagation,
+ * preventDefault. If no value is set no action is performed.
+ */
+ /**
+ * @cfg {Object} [processEventScope=this]
+ * The scope (`this` context) in which the {@link #processEvent} method is executed.
+ */
+ /**
+ * @cfg {Boolean} [ignoreInputFields=false]
+ * Configure this as `true` if there are any input fields within the {@link #target},
+ * and this KeyNav should not process events from input fields
+ * (` `,`