]> git.proxmox.com Git - sencha-touch.git/blob - src/sencha-touch-all-debug.js
import Sencha Touch 2.4.2 source
[sencha-touch.git] / src / sencha-touch-all-debug.js
1 /*
2 This file is part of Sencha Touch 2.4
3
4 Copyright (c) 2011-2015 Sencha Inc
5
6 Contact: http://www.sencha.com/contact
7
8 GNU General Public License Usage
9 This file may be used under the terms of the GNU General Public License version 3.0 as
10 published by the Free Software Foundation and appearing in the file LICENSE included in the
11 packaging of this file.
12
13 Please review the following information to ensure the GNU General Public License version 3.0
14 requirements will be met: http://www.gnu.org/copyleft/gpl.html.
15
16 If you are unsure which license is appropriate for your use, please contact the sales department
17 at http://www.sencha.com/contact.
18
19 Build date: 2015-06-10 14:41:48 (dd5f81fb46d0676281fdd021ada1da3ef06abd27)
20 */
21 //@tag foundation,core
22 //@define Ext
23
24 /**
25 * @class Ext
26 * @singleton
27 */
28 (function() {
29 var global = this,
30 objectPrototype = Object.prototype,
31 toString = objectPrototype.toString,
32 enumerables = true,
33 enumerablesTest = { toString: 1 },
34 emptyFn = function(){},
35 i;
36
37 if (typeof Ext === 'undefined') {
38 global.Ext = {};
39 }
40
41 Ext.global = global;
42
43 for (i in enumerablesTest) {
44 enumerables = null;
45 }
46
47 if (enumerables) {
48 enumerables = ['hasOwnProperty', 'valueOf', 'isPrototypeOf', 'propertyIsEnumerable',
49 'toLocaleString', 'toString', 'constructor'];
50 }
51
52 /**
53 * An array containing extra enumerables for old browsers.
54 * @property {String[]}
55 */
56 Ext.enumerables = enumerables;
57
58 /**
59 * Copies all the properties of config to the specified object.
60 * Note that if recursive merging and cloning without referencing the original objects / arrays is needed, use
61 * {@link Ext.Object#merge} instead.
62 * @param {Object} object The receiver of the properties.
63 * @param {Object} config The source of the properties.
64 * @param {Object} [defaults] A different object that will also be applied for default values.
65 * @return {Object} returns obj
66 */
67 Ext.apply = function(object, config, defaults) {
68 if (defaults) {
69 Ext.apply(object, defaults);
70 }
71
72 if (object && config && typeof config === 'object') {
73 var i, j, k;
74
75 for (i in config) {
76 object[i] = config[i];
77 }
78
79 if (enumerables) {
80 for (j = enumerables.length; j--;) {
81 k = enumerables[j];
82 if (config.hasOwnProperty(k)) {
83 object[k] = config[k];
84 }
85 }
86 }
87 }
88
89 return object;
90 };
91
92 Ext.buildSettings = Ext.apply({
93 baseCSSPrefix: 'x-',
94 scopeResetCSS: false
95 }, Ext.buildSettings || {});
96
97 Ext.apply(Ext, {
98 /**
99 * @property {Function}
100 * A reusable empty function
101 */
102 emptyFn: emptyFn,
103
104 baseCSSPrefix: Ext.buildSettings.baseCSSPrefix,
105
106 /**
107 * Copies all the properties of config to object if they don't already exist.
108 * @param {Object} object The receiver of the properties.
109 * @param {Object} config The source of the properties.
110 * @return {Object} returns obj
111 */
112 applyIf: function(object, config) {
113 var property;
114
115 if (object) {
116 for (property in config) {
117 if (object[property] === undefined) {
118 object[property] = config[property];
119 }
120 }
121 }
122
123 return object;
124 },
125
126 /**
127 * Iterates either an array or an object. This method delegates to
128 * {@link Ext.Array#each Ext.Array.each} if the given value is iterable, and {@link Ext.Object#each Ext.Object.each} otherwise.
129 *
130 * @param {Object/Array} object The object or array to be iterated.
131 * @param {Function} fn The function to be called for each iteration. See and {@link Ext.Array#each Ext.Array.each} and
132 * {@link Ext.Object#each Ext.Object.each} for detailed lists of arguments passed to this function depending on the given object
133 * type that is being iterated.
134 * @param {Object} scope (Optional) The scope (`this` reference) in which the specified function is executed.
135 * Defaults to the object being iterated itself.
136 */
137 iterate: function(object, fn, scope) {
138 if (Ext.isEmpty(object)) {
139 return;
140 }
141
142 if (scope === undefined) {
143 scope = object;
144 }
145
146 if (Ext.isIterable(object)) {
147 Ext.Array.each.call(Ext.Array, object, fn, scope);
148 }
149 else {
150 Ext.Object.each.call(Ext.Object, object, fn, scope);
151 }
152 }
153 });
154
155 Ext.apply(Ext, {
156
157 /**
158 * This method deprecated. Use {@link Ext#define Ext.define} instead.
159 * @method
160 * @param {Function} superclass
161 * @param {Object} overrides
162 * @return {Function} The subclass constructor from the `overrides` parameter, or a generated one if not provided.
163 * @deprecated 2.0.0 Please use {@link Ext#define Ext.define} instead
164 */
165 extend: function() {
166 // inline overrides
167 var objectConstructor = objectPrototype.constructor,
168 inlineOverrides = function(o) {
169 for (var m in o) {
170 if (!o.hasOwnProperty(m)) {
171 continue;
172 }
173 this[m] = o[m];
174 }
175 };
176
177 return function(subclass, superclass, overrides) {
178 // First we check if the user passed in just the superClass with overrides
179 if (Ext.isObject(superclass)) {
180 overrides = superclass;
181 superclass = subclass;
182 subclass = overrides.constructor !== objectConstructor ? overrides.constructor : function() {
183 superclass.apply(this, arguments);
184 };
185 }
186
187 //<debug>
188 if (!superclass) {
189 Ext.Error.raise({
190 sourceClass: 'Ext',
191 sourceMethod: 'extend',
192 msg: 'Attempting to extend from a class which has not been loaded on the page.'
193 });
194 }
195 //</debug>
196
197 // We create a new temporary class
198 var F = function() {},
199 subclassProto, superclassProto = superclass.prototype;
200
201 F.prototype = superclassProto;
202 subclassProto = subclass.prototype = new F();
203 subclassProto.constructor = subclass;
204 subclass.superclass = superclassProto;
205
206 if (superclassProto.constructor === objectConstructor) {
207 superclassProto.constructor = superclass;
208 }
209
210 subclass.override = function(overrides) {
211 Ext.override(subclass, overrides);
212 };
213
214 subclassProto.override = inlineOverrides;
215 subclassProto.proto = subclassProto;
216
217 subclass.override(overrides);
218 subclass.extend = function(o) {
219 return Ext.extend(subclass, o);
220 };
221
222 return subclass;
223 };
224 }(),
225
226 /**
227 * Proxy to {@link Ext.Base#override}. Please refer {@link Ext.Base#override} for further details.
228 *
229 * @param {Object} cls The class to override
230 * @param {Object} overrides The properties to add to `origClass`. This should be specified as an object literal
231 * containing one or more properties.
232 * @method override
233 * @deprecated 2.0.0 Please use {@link Ext#define Ext.define} instead.
234 */
235 override: function(cls, overrides) {
236 if (cls.$isClass) {
237 return cls.override(overrides);
238 }
239 else {
240 Ext.apply(cls.prototype, overrides);
241 }
242 }
243 });
244
245 // A full set of static methods to do type checking
246 Ext.apply(Ext, {
247
248 /**
249 * Returns the given value itself if it's not empty, as described in {@link Ext#isEmpty}; returns the default
250 * value (second argument) otherwise.
251 *
252 * @param {Object} value The value to test.
253 * @param {Object} defaultValue The value to return if the original value is empty.
254 * @param {Boolean} [allowBlank=false] (optional) `true` to allow zero length strings to qualify as non-empty.
255 * @return {Object} `value`, if non-empty, else `defaultValue`.
256 */
257 valueFrom: function(value, defaultValue, allowBlank){
258 return Ext.isEmpty(value, allowBlank) ? defaultValue : value;
259 },
260
261 /**
262 * Returns the type of the given variable in string format. List of possible values are:
263 *
264 * - `undefined`: If the given value is `undefined`
265 * - `null`: If the given value is `null`
266 * - `string`: If the given value is a string
267 * - `number`: If the given value is a number
268 * - `boolean`: If the given value is a boolean value
269 * - `date`: If the given value is a `Date` object
270 * - `function`: If the given value is a function reference
271 * - `object`: If the given value is an object
272 * - `array`: If the given value is an array
273 * - `regexp`: If the given value is a regular expression
274 * - `element`: If the given value is a DOM Element
275 * - `textnode`: If the given value is a DOM text node and contains something other than whitespace
276 * - `whitespace`: If the given value is a DOM text node and contains only whitespace
277 *
278 * @param {Object} value
279 * @return {String}
280 */
281 typeOf: function(value) {
282 if (value === null) {
283 return 'null';
284 }
285
286 var type = typeof value;
287
288 if (type === 'undefined' || type === 'string' || type === 'number' || type === 'boolean') {
289 return type;
290 }
291
292 var typeToString = toString.call(value);
293
294 switch(typeToString) {
295 case '[object Array]':
296 return 'array';
297 case '[object Date]':
298 return 'date';
299 case '[object Boolean]':
300 return 'boolean';
301 case '[object Number]':
302 return 'number';
303 case '[object RegExp]':
304 return 'regexp';
305 }
306
307 if (type === 'function') {
308 return 'function';
309 }
310
311 if (type === 'object') {
312 if (value.nodeType !== undefined) {
313 if (value.nodeType === 3) {
314 return (/\S/).test(value.nodeValue) ? 'textnode' : 'whitespace';
315 }
316 else {
317 return 'element';
318 }
319 }
320
321 return 'object';
322 }
323
324 //<debug error>
325 Ext.Error.raise({
326 sourceClass: 'Ext',
327 sourceMethod: 'typeOf',
328 msg: 'Failed to determine the type of the specified value "' + value + '". This is most likely a bug.'
329 });
330 //</debug>
331 },
332
333 /**
334 * Returns `true` if the passed value is empty, `false` otherwise. The value is deemed to be empty if it is either:
335 *
336 * - `null`
337 * - `undefined`
338 * - a zero-length array.
339 * - a zero-length string (Unless the `allowEmptyString` parameter is set to `true`).
340 *
341 * @param {Object} value The value to test.
342 * @param {Boolean} [allowEmptyString=false] (optional) `true` to allow empty strings.
343 * @return {Boolean}
344 */
345 isEmpty: function(value, allowEmptyString) {
346 return (value === null) || (value === undefined) || (!allowEmptyString ? value === '' : false) || (Ext.isArray(value) && value.length === 0);
347 },
348
349 /**
350 * Returns `true` if the passed value is a JavaScript Array, `false` otherwise.
351 *
352 * @param {Object} target The target to test.
353 * @return {Boolean}
354 * @method
355 */
356 isArray: ('isArray' in Array) ? Array.isArray : function(value) {
357 return toString.call(value) === '[object Array]';
358 },
359
360 /**
361 * Returns `true` if the passed value is a JavaScript Date object, `false` otherwise.
362 * @param {Object} object The object to test.
363 * @return {Boolean}
364 */
365 isDate: function(value) {
366 return toString.call(value) === '[object Date]';
367 },
368
369 /**
370 * Returns 'true' if the passed value is a String that matches the MS Date JSON encoding format
371 * @param {String} value The string to test
372 * @return {Boolean}
373 */
374 isMSDate: function(value) {
375 if (!Ext.isString(value)) {
376 return false;
377 } else {
378 return value.match("\\\\?/Date\\(([-+])?(\\d+)(?:[+-]\\d{4})?\\)\\\\?/") !== null;
379 }
380 },
381
382 /**
383 * Returns `true` if the passed value is a JavaScript Object, `false` otherwise.
384 * @param {Object} value The value to test.
385 * @return {Boolean}
386 * @method
387 */
388 isObject: (toString.call(null) === '[object Object]') ?
389 function(value) {
390 // check ownerDocument here as well to exclude DOM nodes
391 return value !== null && value !== undefined && toString.call(value) === '[object Object]' && value.ownerDocument === undefined;
392 } :
393 function(value) {
394 return toString.call(value) === '[object Object]';
395 },
396
397 /**
398 * @private
399 */
400 isSimpleObject: function(value) {
401 return value instanceof Object && value.constructor === Object;
402 },
403 /**
404 * Returns `true` if the passed value is a JavaScript 'primitive', a string, number or Boolean.
405 * @param {Object} value The value to test.
406 * @return {Boolean}
407 */
408 isPrimitive: function(value) {
409 var type = typeof value;
410
411 return type === 'string' || type === 'number' || type === 'boolean';
412 },
413
414 /**
415 * Returns `true` if the passed value is a JavaScript Function, `false` otherwise.
416 * @param {Object} value The value to test.
417 * @return {Boolean}
418 * @method
419 */
420 isFunction:
421 // Safari 3.x and 4.x returns 'function' for typeof <NodeList>, hence we need to fall back to using
422 // Object.prorotype.toString (slower)
423 (typeof document !== 'undefined' && typeof document.getElementsByTagName('body') === 'function') ? function(value) {
424 return toString.call(value) === '[object Function]';
425 } : function(value) {
426 return typeof value === 'function';
427 },
428
429 /**
430 * Returns `true` if the passed value is a number. Returns `false` for non-finite numbers.
431 * @param {Object} value The value to test.
432 * @return {Boolean}
433 */
434 isNumber: function(value) {
435 return typeof value === 'number' && isFinite(value);
436 },
437
438 /**
439 * Validates that a value is numeric.
440 * @param {Object} value Examples: 1, '1', '2.34'
441 * @return {Boolean} `true` if numeric, `false` otherwise.
442 */
443 isNumeric: function(value) {
444 return !isNaN(parseFloat(value)) && isFinite(value);
445 },
446
447 /**
448 * Returns `true` if the passed value is a string.
449 * @param {Object} value The value to test.
450 * @return {Boolean}
451 */
452 isString: function(value) {
453 return typeof value === 'string';
454 },
455
456 /**
457 * Returns `true` if the passed value is a Boolean.
458 *
459 * @param {Object} value The value to test.
460 * @return {Boolean}
461 */
462 isBoolean: function(value) {
463 return typeof value === 'boolean';
464 },
465
466 /**
467 * Returns `true` if the passed value is an HTMLElement.
468 * @param {Object} value The value to test.
469 * @return {Boolean}
470 */
471 isElement: function(value) {
472 return value ? value.nodeType === 1 : false;
473 },
474
475 /**
476 * Returns `true` if the passed value is a TextNode.
477 * @param {Object} value The value to test.
478 * @return {Boolean}
479 */
480 isTextNode: function(value) {
481 return value ? value.nodeName === "#text" : false;
482 },
483
484 /**
485 * Returns `true` if the passed value is defined.
486 * @param {Object} value The value to test.
487 * @return {Boolean}
488 */
489 isDefined: function(value) {
490 return typeof value !== 'undefined';
491 },
492
493 /**
494 * Returns `true` if the passed value is iterable, `false` otherwise.
495 * @param {Object} value The value to test.
496 * @return {Boolean}
497 */
498 isIterable: function(value) {
499 return (value && typeof value !== 'string') ? value.length !== undefined : false;
500 }
501 });
502
503 Ext.apply(Ext, {
504
505 /**
506 * Clone almost any type of variable including array, object, DOM nodes and Date without keeping the old reference.
507 * @param {Object} item The variable to clone.
508 * @return {Object} clone
509 */
510 clone: function(item) {
511 if (item === null || item === undefined) {
512 return item;
513 }
514
515 // DOM nodes
516 if (item.nodeType && item.cloneNode) {
517 return item.cloneNode(true);
518 }
519
520 // Strings
521 var type = toString.call(item);
522
523 // Dates
524 if (type === '[object Date]') {
525 return new Date(item.getTime());
526 }
527
528 var i, j, k, clone, key;
529
530 // Arrays
531 if (type === '[object Array]') {
532 i = item.length;
533
534 clone = [];
535
536 while (i--) {
537 clone[i] = Ext.clone(item[i]);
538 }
539 }
540 // Objects
541 else if (type === '[object Object]' && item.constructor === Object) {
542 clone = {};
543
544 for (key in item) {
545 clone[key] = Ext.clone(item[key]);
546 }
547
548 if (enumerables) {
549 for (j = enumerables.length; j--;) {
550 k = enumerables[j];
551 clone[k] = item[k];
552 }
553 }
554 }
555
556 return clone || item;
557 },
558
559 /**
560 * @private
561 * Generate a unique reference of Ext in the global scope, useful for sandboxing.
562 */
563 getUniqueGlobalNamespace: function() {
564 var uniqueGlobalNamespace = this.uniqueGlobalNamespace;
565
566 if (uniqueGlobalNamespace === undefined) {
567 var i = 0;
568
569 do {
570 uniqueGlobalNamespace = 'ExtBox' + (++i);
571 } while (Ext.global[uniqueGlobalNamespace] !== undefined);
572
573 Ext.global[uniqueGlobalNamespace] = Ext;
574 this.uniqueGlobalNamespace = uniqueGlobalNamespace;
575 }
576
577 return uniqueGlobalNamespace;
578 },
579
580 /**
581 * @private
582 */
583 functionFactory: function() {
584 var args = Array.prototype.slice.call(arguments),
585 ln = args.length;
586
587 if (ln > 0) {
588 args[ln - 1] = 'var Ext=window.' + this.getUniqueGlobalNamespace() + ';' + args[ln - 1];
589 }
590
591 return Function.prototype.constructor.apply(Function.prototype, args);
592 },
593
594 /**
595 * @private
596 */
597 globalEval: ('execScript' in global) ? function(code) {
598 global.execScript(code)
599 } : function(code) {
600 (function(){
601 eval(code);
602 })();
603 }
604
605 //<feature logger>
606 /**
607 * @private
608 * @property
609 */
610 ,Logger: {
611 log: function(message, priority) {
612 if ('console' in global) {
613 if (!priority || !(priority in global.console)) {
614 priority = 'log';
615 }
616 message = '[' + priority.toUpperCase() + '] ' + message;
617 global.console[priority](message);
618 }
619 },
620 verbose: function(message) {
621 this.log(message, 'verbose');
622 },
623 info: function(message) {
624 this.log(message, 'info');
625 },
626 warn: function(message) {
627 this.log(message, 'warn');
628 },
629 error: function(message) {
630 throw new Error(message);
631 },
632 deprecate: function(message) {
633 this.log(message, 'warn');
634 }
635 }
636 //</feature>
637 });
638
639 /**
640 * Old alias to {@link Ext#typeOf}.
641 * @deprecated 2.0.0 Please use {@link Ext#typeOf} instead.
642 * @method
643 * @alias Ext#typeOf
644 */
645 Ext.type = Ext.typeOf;
646
647 })();
648
649 //@tag foundation,core
650 //@define Ext.Version
651 //@require Ext
652
653 /**
654 * @author Jacky Nguyen <jacky@sencha.com>
655 * @docauthor Jacky Nguyen <jacky@sencha.com>
656 * @class Ext.Version
657 *
658 * A utility class that wrap around a string version number and provide convenient
659 * method to perform comparison. See also: {@link Ext.Version#compare compare}. Example:
660 *
661 * var version = new Ext.Version('1.0.2beta');
662 * console.log("Version is " + version); // Version is 1.0.2beta
663 *
664 * console.log(version.getMajor()); // 1
665 * console.log(version.getMinor()); // 0
666 * console.log(version.getPatch()); // 2
667 * console.log(version.getBuild()); // 0
668 * console.log(version.getRelease()); // beta
669 *
670 * console.log(version.isGreaterThan('1.0.1')); // true
671 * console.log(version.isGreaterThan('1.0.2alpha')); // true
672 * console.log(version.isGreaterThan('1.0.2RC')); // false
673 * console.log(version.isGreaterThan('1.0.2')); // false
674 * console.log(version.isLessThan('1.0.2')); // true
675 *
676 * console.log(version.match(1.0)); // true
677 * console.log(version.match('1.0.2')); // true
678 */
679 (function() {
680
681 // Current core version
682 var version = '2.4.2.571', Version;
683 Ext.Version = Version = Ext.extend(Object, {
684
685 /**
686 * Creates new Version object.
687 * @param {String/Number} version The version number in the follow standard format: major[.minor[.patch[.build[release]]]]
688 * Examples: 1.0 or 1.2.3beta or 1.2.3.4RC
689 * @return {Ext.Version} this
690 */
691 constructor: function(version) {
692 var toNumber = this.toNumber,
693 parts, releaseStartIndex;
694
695 if (version instanceof Version) {
696 return version;
697 }
698
699 this.version = this.shortVersion = String(version).toLowerCase().replace(/_/g, '.').replace(/[\-+]/g, '');
700
701 releaseStartIndex = this.version.search(/([^\d\.])/);
702
703 if (releaseStartIndex !== -1) {
704 this.release = this.version.substr(releaseStartIndex, version.length);
705 this.shortVersion = this.version.substr(0, releaseStartIndex);
706 }
707
708 this.shortVersion = this.shortVersion.replace(/[^\d]/g, '');
709
710 parts = this.version.split('.');
711
712 this.major = toNumber(parts.shift());
713 this.minor = toNumber(parts.shift());
714 this.patch = toNumber(parts.shift());
715 this.build = toNumber(parts.shift());
716
717 return this;
718 },
719
720 /**
721 * @param {Number} value
722 * @return {Number}
723 */
724 toNumber: function(value) {
725 value = parseInt(value || 0, 10);
726
727 if (isNaN(value)) {
728 value = 0;
729 }
730
731 return value;
732 },
733
734 /**
735 * Override the native `toString()` method.
736 * @private
737 * @return {String} version
738 */
739 toString: function() {
740 return this.version;
741 },
742
743 /**
744 * Override the native `valueOf()` method.
745 * @private
746 * @return {String} version
747 */
748 valueOf: function() {
749 return this.version;
750 },
751
752 /**
753 * Returns the major component value.
754 * @return {Number} major
755 */
756 getMajor: function() {
757 return this.major || 0;
758 },
759
760 /**
761 * Returns the minor component value.
762 * @return {Number} minor
763 */
764 getMinor: function() {
765 return this.minor || 0;
766 },
767
768 /**
769 * Returns the patch component value.
770 * @return {Number} patch
771 */
772 getPatch: function() {
773 return this.patch || 0;
774 },
775
776 /**
777 * Returns the build component value.
778 * @return {Number} build
779 */
780 getBuild: function() {
781 return this.build || 0;
782 },
783
784 /**
785 * Returns the release component value.
786 * @return {Number} release
787 */
788 getRelease: function() {
789 return this.release || '';
790 },
791
792 /**
793 * Returns whether this version if greater than the supplied argument.
794 * @param {String/Number} target The version to compare with.
795 * @return {Boolean} `true` if this version if greater than the target, `false` otherwise.
796 */
797 isGreaterThan: function(target) {
798 return Version.compare(this.version, target) === 1;
799 },
800
801 /**
802 * Returns whether this version if greater than or equal to the supplied argument.
803 * @param {String/Number} target The version to compare with.
804 * @return {Boolean} `true` if this version if greater than or equal to the target, `false` otherwise.
805 */
806 isGreaterThanOrEqual: function(target) {
807 return Version.compare(this.version, target) >= 0;
808 },
809
810 /**
811 * Returns whether this version if smaller than the supplied argument.
812 * @param {String/Number} target The version to compare with.
813 * @return {Boolean} `true` if this version if smaller than the target, `false` otherwise.
814 */
815 isLessThan: function(target) {
816 return Version.compare(this.version, target) === -1;
817 },
818
819 /**
820 * Returns whether this version if less than or equal to the supplied argument.
821 * @param {String/Number} target The version to compare with.
822 * @return {Boolean} `true` if this version if less than or equal to the target, `false` otherwise.
823 */
824 isLessThanOrEqual: function(target) {
825 return Version.compare(this.version, target) <= 0;
826 },
827
828 /**
829 * Returns whether this version equals to the supplied argument.
830 * @param {String/Number} target The version to compare with.
831 * @return {Boolean} `true` if this version equals to the target, `false` otherwise.
832 */
833 equals: function(target) {
834 return Version.compare(this.version, target) === 0;
835 },
836
837 /**
838 * Returns whether this version matches the supplied argument. Example:
839 *
840 * var version = new Ext.Version('1.0.2beta');
841 * console.log(version.match(1)); // true
842 * console.log(version.match(1.0)); // true
843 * console.log(version.match('1.0.2')); // true
844 * console.log(version.match('1.0.2RC')); // false
845 *
846 * @param {String/Number} target The version to compare with.
847 * @return {Boolean} `true` if this version matches the target, `false` otherwise.
848 */
849 match: function(target) {
850 target = String(target);
851 return this.version.substr(0, target.length) === target;
852 },
853
854 /**
855 * Returns this format: [major, minor, patch, build, release]. Useful for comparison.
856 * @return {Number[]}
857 */
858 toArray: function() {
859 return [this.getMajor(), this.getMinor(), this.getPatch(), this.getBuild(), this.getRelease()];
860 },
861
862 /**
863 * Returns shortVersion version without dots and release.
864 * @return {String}
865 */
866 getShortVersion: function() {
867 return this.shortVersion;
868 },
869
870 /**
871 * Convenient alias to {@link Ext.Version#isGreaterThan isGreaterThan}
872 * @param {String/Number} target
873 * @return {Boolean}
874 */
875 gt: function() {
876 return this.isGreaterThan.apply(this, arguments);
877 },
878
879 /**
880 * Convenient alias to {@link Ext.Version#isLessThan isLessThan}
881 * @param {String/Number} target
882 * @return {Boolean}
883 */
884 lt: function() {
885 return this.isLessThan.apply(this, arguments);
886 },
887
888 /**
889 * Convenient alias to {@link Ext.Version#isGreaterThanOrEqual isGreaterThanOrEqual}
890 * @param {String/Number} target
891 * @return {Boolean}
892 */
893 gtEq: function() {
894 return this.isGreaterThanOrEqual.apply(this, arguments);
895 },
896
897 /**
898 * Convenient alias to {@link Ext.Version#isLessThanOrEqual isLessThanOrEqual}
899 * @param {String/Number} target
900 * @return {Boolean}
901 */
902 ltEq: function() {
903 return this.isLessThanOrEqual.apply(this, arguments);
904 }
905 });
906
907 Ext.apply(Version, {
908 // @private
909 releaseValueMap: {
910 'dev': -6,
911 'alpha': -5,
912 'a': -5,
913 'beta': -4,
914 'b': -4,
915 'rc': -3,
916 '#': -2,
917 'p': -1,
918 'pl': -1
919 },
920
921 /**
922 * Converts a version component to a comparable value.
923 *
924 * @static
925 * @param {Object} value The value to convert
926 * @return {Object}
927 */
928 getComponentValue: function(value) {
929 return !value ? 0 : (isNaN(value) ? this.releaseValueMap[value] || value : parseInt(value, 10));
930 },
931
932 /**
933 * Compare 2 specified versions, starting from left to right. If a part contains special version strings,
934 * they are handled in the following order:
935 * 'dev' < 'alpha' = 'a' < 'beta' = 'b' < 'RC' = 'rc' < '#' < 'pl' = 'p' < 'anything else'
936 *
937 * @static
938 * @param {String} current The current version to compare to.
939 * @param {String} target The target version to compare to.
940 * @return {Number} Returns -1 if the current version is smaller than the target version, 1 if greater, and 0 if they're equivalent.
941 */
942 compare: function(current, target) {
943 var currentValue, targetValue, i;
944
945 current = new Version(current).toArray();
946 target = new Version(target).toArray();
947
948 for (i = 0; i < Math.max(current.length, target.length); i++) {
949 currentValue = this.getComponentValue(current[i]);
950 targetValue = this.getComponentValue(target[i]);
951
952 if (currentValue < targetValue) {
953 return -1;
954 } else if (currentValue > targetValue) {
955 return 1;
956 }
957 }
958
959 return 0;
960 }
961 });
962
963 Ext.apply(Ext, {
964 /**
965 * @private
966 */
967 versions: {},
968
969 /**
970 * @private
971 */
972 lastRegisteredVersion: null,
973
974 /**
975 * Set version number for the given package name.
976 *
977 * @param {String} packageName The package name, for example: 'core', 'touch', 'extjs'.
978 * @param {String/Ext.Version} version The version, for example: '1.2.3alpha', '2.4.0-dev'.
979 * @return {Ext}
980 */
981 setVersion: function(packageName, version) {
982 Ext.versions[packageName] = new Version(version);
983 Ext.lastRegisteredVersion = Ext.versions[packageName];
984
985 return this;
986 },
987
988 /**
989 * Get the version number of the supplied package name; will return the last registered version
990 * (last `Ext.setVersion()` call) if there's no package name given.
991 *
992 * @param {String} packageName (Optional) The package name, for example: 'core', 'touch', 'extjs'.
993 * @return {Ext.Version} The version.
994 */
995 getVersion: function(packageName) {
996 if (packageName === undefined) {
997 return Ext.lastRegisteredVersion;
998 }
999
1000 return Ext.versions[packageName];
1001 },
1002
1003 /**
1004 * Create a closure for deprecated code.
1005 *
1006 * // This means Ext.oldMethod is only supported in 4.0.0beta and older.
1007 * // If Ext.getVersion('extjs') returns a version that is later than '4.0.0beta', for example '4.0.0RC',
1008 * // the closure will not be invoked
1009 * Ext.deprecate('extjs', '4.0.0beta', function() {
1010 * Ext.oldMethod = Ext.newMethod;
1011 * // ...
1012 * });
1013 *
1014 * @param {String} packageName The package name.
1015 * @param {String} since The last version before it's deprecated.
1016 * @param {Function} closure The callback function to be executed with the specified version is less than the current version.
1017 * @param {Object} scope The execution scope (`this`) if the closure
1018 */
1019 deprecate: function(packageName, since, closure, scope) {
1020 if (Version.compare(Ext.getVersion(packageName), since) < 1) {
1021 closure.call(scope);
1022 }
1023 }
1024 }); // End Versioning
1025
1026 Ext.setVersion('core', version);
1027
1028 })();
1029
1030 //@tag foundation,core
1031 //@define Ext.String
1032 //@require Ext.Version
1033
1034 /**
1035 * @class Ext.String
1036 *
1037 * A collection of useful static methods to deal with strings.
1038 * @singleton
1039 */
1040
1041 Ext.String = {
1042 trimRegex: /^[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000]+|[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000]+$/g,
1043 escapeRe: /('|\\)/g,
1044 formatRe: /\{(\d+)\}/g,
1045 escapeRegexRe: /([-.*+?^${}()|[\]\/\\])/g,
1046
1047 /**
1048 * Convert certain characters (&, <, >, and ") to their HTML character equivalents for literal display in web pages.
1049 * @param {String} value The string to encode.
1050 * @return {String} The encoded text.
1051 * @method
1052 */
1053 htmlEncode: (function() {
1054 var entities = {
1055 '&': '&amp;',
1056 '>': '&gt;',
1057 '<': '&lt;',
1058 '"': '&quot;'
1059 }, keys = [], p, regex;
1060
1061 for (p in entities) {
1062 keys.push(p);
1063 }
1064
1065 regex = new RegExp('(' + keys.join('|') + ')', 'g');
1066
1067 return function(value) {
1068 return (!value) ? value : String(value).replace(regex, function(match, capture) {
1069 return entities[capture];
1070 });
1071 };
1072 })(),
1073
1074 /**
1075 * Convert certain characters (&, <, >, and ") from their HTML character equivalents.
1076 * @param {String} value The string to decode.
1077 * @return {String} The decoded text.
1078 * @method
1079 */
1080 htmlDecode: (function() {
1081 var entities = {
1082 '&amp;': '&',
1083 '&gt;': '>',
1084 '&lt;': '<',
1085 '&quot;': '"'
1086 }, keys = [], p, regex;
1087
1088 for (p in entities) {
1089 keys.push(p);
1090 }
1091
1092 regex = new RegExp('(' + keys.join('|') + '|&#[0-9]{1,5};' + ')', 'g');
1093
1094 return function(value) {
1095 return (!value) ? value : String(value).replace(regex, function(match, capture) {
1096 if (capture in entities) {
1097 return entities[capture];
1098 } else {
1099 return String.fromCharCode(parseInt(capture.substr(2), 10));
1100 }
1101 });
1102 };
1103 })(),
1104
1105 /**
1106 * Appends content to the query string of a URL, handling logic for whether to place
1107 * a question mark or ampersand.
1108 * @param {String} url The URL to append to.
1109 * @param {String} string The content to append to the URL.
1110 * @return {String} The resulting URL.
1111 */
1112 urlAppend : function(url, string) {
1113 if (!Ext.isEmpty(string)) {
1114 return url + (url.indexOf('?') === -1 ? '?' : '&') + string;
1115 }
1116
1117 return url;
1118 },
1119
1120 /**
1121 * Trims whitespace from either end of a string, leaving spaces within the string intact. Example:
1122 *
1123 * @example
1124 * var s = ' foo bar ';
1125 * alert('-' + s + '-'); // alerts "- foo bar -"
1126 * alert('-' + Ext.String.trim(s) + '-'); // alerts "-foo bar-"
1127 *
1128 * @param {String} string The string to escape
1129 * @return {String} The trimmed string
1130 */
1131 trim: function(string) {
1132 return string.replace(Ext.String.trimRegex, "");
1133 },
1134
1135 /**
1136 * Capitalize the given string.
1137 * @param {String} string
1138 * @return {String}
1139 */
1140 capitalize: function(string) {
1141 return string.charAt(0).toUpperCase() + string.substr(1);
1142 },
1143
1144 /**
1145 * Truncate a string and add an ellipsis ('...') to the end if it exceeds the specified length.
1146 * @param {String} value The string to truncate.
1147 * @param {Number} length The maximum length to allow before truncating.
1148 * @param {Boolean} word `true` to try to find a common word break.
1149 * @return {String} The converted text.
1150 */
1151 ellipsis: function(value, len, word) {
1152 if (value && value.length > len) {
1153 if (word) {
1154 var vs = value.substr(0, len - 2),
1155 index = Math.max(vs.lastIndexOf(' '), vs.lastIndexOf('.'), vs.lastIndexOf('!'), vs.lastIndexOf('?'));
1156 if (index !== -1 && index >= (len - 15)) {
1157 return vs.substr(0, index) + "...";
1158 }
1159 }
1160 return value.substr(0, len - 3) + "...";
1161 }
1162 return value;
1163 },
1164
1165 /**
1166 * Escapes the passed string for use in a regular expression.
1167 * @param {String} string
1168 * @return {String}
1169 */
1170 escapeRegex: function(string) {
1171 return string.replace(Ext.String.escapeRegexRe, "\\$1");
1172 },
1173
1174 /**
1175 * Escapes the passed string for ' and \.
1176 * @param {String} string The string to escape.
1177 * @return {String} The escaped string.
1178 */
1179 escape: function(string) {
1180 return string.replace(Ext.String.escapeRe, "\\$1");
1181 },
1182
1183 /**
1184 * Utility function that allows you to easily switch a string between two alternating values. The passed value
1185 * is compared to the current string, and if they are equal, the other value that was passed in is returned. If
1186 * they are already different, the first value passed in is returned. Note that this method returns the new value
1187 * but does not change the current string.
1188 *
1189 * // alternate sort directions
1190 * sort = Ext.String.toggle(sort, 'ASC', 'DESC');
1191 *
1192 * // instead of conditional logic:
1193 * sort = (sort == 'ASC' ? 'DESC' : 'ASC');
1194 *
1195 * @param {String} string The current string.
1196 * @param {String} value The value to compare to the current string.
1197 * @param {String} other The new value to use if the string already equals the first value passed in.
1198 * @return {String} The new value.
1199 */
1200 toggle: function(string, value, other) {
1201 return string === value ? other : value;
1202 },
1203
1204 /**
1205 * Pads the left side of a string with a specified character. This is especially useful
1206 * for normalizing number and date strings. Example usage:
1207 *
1208 * var s = Ext.String.leftPad('123', 5, '0');
1209 * alert(s); // '00123'
1210 *
1211 * @param {String} string The original string.
1212 * @param {Number} size The total length of the output string.
1213 * @param {String} [character= ] (optional) The character with which to pad the original string (defaults to empty string " ").
1214 * @return {String} The padded string.
1215 */
1216 leftPad: function(string, size, character) {
1217 var result = String(string);
1218 character = character || " ";
1219 while (result.length < size) {
1220 result = character + result;
1221 }
1222 return result;
1223 },
1224
1225 /**
1226 * Allows you to define a tokenized string and pass an arbitrary number of arguments to replace the tokens. Each
1227 * token must be unique, and must increment in the format {0}, {1}, etc. Example usage:
1228 *
1229 * var cls = 'my-class',
1230 * text = 'Some text';
1231 * var s = Ext.String.format('<div class="{0}">{1}</div>', cls, text);
1232 * alert(s); // '<div class="my-class">Some text</div>'
1233 *
1234 * @param {String} string The tokenized string to be formatted.
1235 * @param {String...} values First param value to replace token `{0}`, then next
1236 * param to replace `{1}` etc.
1237 * @return {String} The formatted string.
1238 */
1239 format: function(format) {
1240 var args = Ext.Array.toArray(arguments, 1);
1241 return format.replace(Ext.String.formatRe, function(m, i) {
1242 return args[i];
1243 });
1244 },
1245
1246 /**
1247 * Returns a string with a specified number of repetitions a given string pattern.
1248 * The pattern be separated by a different string.
1249 *
1250 * var s = Ext.String.repeat('---', 4); // '------------'
1251 * var t = Ext.String.repeat('--', 3, '/'); // '--/--/--'
1252 *
1253 * @param {String} pattern The pattern to repeat.
1254 * @param {Number} count The number of times to repeat the pattern (may be 0).
1255 * @param {String} sep An option string to separate each pattern.
1256 */
1257 repeat: function(pattern, count, sep) {
1258 for (var buf = [], i = count; i--; ) {
1259 buf.push(pattern);
1260 }
1261 return buf.join(sep || '');
1262 }
1263 };
1264
1265 /**
1266 * Old alias to {@link Ext.String#htmlEncode}.
1267 * @deprecated Use {@link Ext.String#htmlEncode} instead.
1268 * @method
1269 * @member Ext
1270 * @alias Ext.String#htmlEncode
1271 */
1272 Ext.htmlEncode = Ext.String.htmlEncode;
1273
1274
1275 /**
1276 * Old alias to {@link Ext.String#htmlDecode}.
1277 * @deprecated Use {@link Ext.String#htmlDecode} instead.
1278 * @method
1279 * @member Ext
1280 * @alias Ext.String#htmlDecode
1281 */
1282 Ext.htmlDecode = Ext.String.htmlDecode;
1283
1284 /**
1285 * Old alias to {@link Ext.String#urlAppend}.
1286 * @deprecated Use {@link Ext.String#urlAppend} instead.
1287 * @method
1288 * @member Ext
1289 * @alias Ext.String#urlAppend
1290 */
1291 Ext.urlAppend = Ext.String.urlAppend;
1292
1293 //@tag foundation,core
1294 //@define Ext.Array
1295 //@require Ext.String
1296
1297 /**
1298 * @class Ext.Array
1299 * @singleton
1300 * @author Jacky Nguyen <jacky@sencha.com>
1301 * @docauthor Jacky Nguyen <jacky@sencha.com>
1302 *
1303 * A set of useful static methods to deal with arrays; provide missing methods for older browsers.
1304 */
1305 (function() {
1306
1307 var arrayPrototype = Array.prototype,
1308 slice = arrayPrototype.slice,
1309 supportsSplice = function () {
1310 var array = [],
1311 lengthBefore,
1312 j = 20;
1313
1314 if (!array.splice) {
1315 return false;
1316 }
1317
1318 // This detects a bug in IE8 splice method:
1319 // see http://social.msdn.microsoft.com/Forums/en-US/iewebdevelopment/thread/6e946d03-e09f-4b22-a4dd-cd5e276bf05a/
1320
1321 while (j--) {
1322 array.push("A");
1323 }
1324
1325 array.splice(15, 0, "F", "F", "F", "F", "F","F","F","F","F","F","F","F","F","F","F","F","F","F","F","F","F");
1326
1327 lengthBefore = array.length; //41
1328 array.splice(13, 0, "XXX"); // add one element
1329
1330 if (lengthBefore+1 != array.length) {
1331 return false;
1332 }
1333 // end IE8 bug
1334
1335 return true;
1336 }(),
1337 supportsForEach = 'forEach' in arrayPrototype,
1338 supportsMap = 'map' in arrayPrototype,
1339 supportsIndexOf = 'indexOf' in arrayPrototype,
1340 supportsEvery = 'every' in arrayPrototype,
1341 supportsSome = 'some' in arrayPrototype,
1342 supportsFilter = 'filter' in arrayPrototype,
1343 supportsSort = function() {
1344 var a = [1,2,3,4,5].sort(function(){ return 0; });
1345 return a[0] === 1 && a[1] === 2 && a[2] === 3 && a[3] === 4 && a[4] === 5;
1346 }(),
1347 supportsSliceOnNodeList = true,
1348 ExtArray;
1349
1350 try {
1351 // IE 6 - 8 will throw an error when using Array.prototype.slice on NodeList
1352 if (typeof document !== 'undefined') {
1353 slice.call(document.getElementsByTagName('body'));
1354 }
1355 } catch (e) {
1356 supportsSliceOnNodeList = false;
1357 }
1358
1359 function fixArrayIndex (array, index) {
1360 return (index < 0) ? Math.max(0, array.length + index)
1361 : Math.min(array.length, index);
1362 }
1363
1364 /*
1365 Does the same work as splice, but with a slightly more convenient signature. The splice
1366 method has bugs in IE8, so this is the implementation we use on that platform.
1367
1368 The rippling of items in the array can be tricky. Consider two use cases:
1369
1370 index=2
1371 removeCount=2
1372 /=====\
1373 +---+---+---+---+---+---+---+---+
1374 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
1375 +---+---+---+---+---+---+---+---+
1376 / \/ \/ \/ \
1377 / /\ /\ /\ \
1378 / / \/ \/ \ +--------------------------+
1379 / / /\ /\ +--------------------------+ \
1380 / / / \/ +--------------------------+ \ \
1381 / / / /+--------------------------+ \ \ \
1382 / / / / \ \ \ \
1383 v v v v v v v v
1384 +---+---+---+---+---+---+ +---+---+---+---+---+---+---+---+---+
1385 | 0 | 1 | 4 | 5 | 6 | 7 | | 0 | 1 | a | b | c | 4 | 5 | 6 | 7 |
1386 +---+---+---+---+---+---+ +---+---+---+---+---+---+---+---+---+
1387 A B \=========/
1388 insert=[a,b,c]
1389
1390 In case A, it is obvious that copying of [4,5,6,7] must be left-to-right so
1391 that we don't end up with [0,1,6,7,6,7]. In case B, we have the opposite; we
1392 must go right-to-left or else we would end up with [0,1,a,b,c,4,4,4,4].
1393 */
1394 function replaceSim (array, index, removeCount, insert) {
1395 var add = insert ? insert.length : 0,
1396 length = array.length,
1397 pos = fixArrayIndex(array, index);
1398
1399 // we try to use Array.push when we can for efficiency...
1400 if (pos === length) {
1401 if (add) {
1402 array.push.apply(array, insert);
1403 }
1404 } else {
1405 var remove = Math.min(removeCount, length - pos),
1406 tailOldPos = pos + remove,
1407 tailNewPos = tailOldPos + add - remove,
1408 tailCount = length - tailOldPos,
1409 lengthAfterRemove = length - remove,
1410 i;
1411
1412 if (tailNewPos < tailOldPos) { // case A
1413 for (i = 0; i < tailCount; ++i) {
1414 array[tailNewPos+i] = array[tailOldPos+i];
1415 }
1416 } else if (tailNewPos > tailOldPos) { // case B
1417 for (i = tailCount; i--; ) {
1418 array[tailNewPos+i] = array[tailOldPos+i];
1419 }
1420 } // else, add == remove (nothing to do)
1421
1422 if (add && pos === lengthAfterRemove) {
1423 array.length = lengthAfterRemove; // truncate array
1424 array.push.apply(array, insert);
1425 } else {
1426 array.length = lengthAfterRemove + add; // reserves space
1427 for (i = 0; i < add; ++i) {
1428 array[pos+i] = insert[i];
1429 }
1430 }
1431 }
1432
1433 return array;
1434 }
1435
1436 function replaceNative (array, index, removeCount, insert) {
1437 if (insert && insert.length) {
1438 if (index < array.length) {
1439 array.splice.apply(array, [index, removeCount].concat(insert));
1440 } else {
1441 array.push.apply(array, insert);
1442 }
1443 } else {
1444 array.splice(index, removeCount);
1445 }
1446 return array;
1447 }
1448
1449 function eraseSim (array, index, removeCount) {
1450 return replaceSim(array, index, removeCount);
1451 }
1452
1453 function eraseNative (array, index, removeCount) {
1454 array.splice(index, removeCount);
1455 return array;
1456 }
1457
1458 function spliceSim (array, index, removeCount) {
1459 var pos = fixArrayIndex(array, index),
1460 removed = array.slice(index, fixArrayIndex(array, pos+removeCount));
1461
1462 if (arguments.length < 4) {
1463 replaceSim(array, pos, removeCount);
1464 } else {
1465 replaceSim(array, pos, removeCount, slice.call(arguments, 3));
1466 }
1467
1468 return removed;
1469 }
1470
1471 function spliceNative (array) {
1472 return array.splice.apply(array, slice.call(arguments, 1));
1473 }
1474
1475 var erase = supportsSplice ? eraseNative : eraseSim,
1476 replace = supportsSplice ? replaceNative : replaceSim,
1477 splice = supportsSplice ? spliceNative : spliceSim;
1478
1479 // NOTE: from here on, use erase, replace or splice (not native methods)...
1480 ExtArray = Ext.Array = {
1481 /**
1482 * Iterates an array or an iterable value and invoke the given callback function for each item.
1483 *
1484 * var countries = ['Vietnam', 'Singapore', 'United States', 'Russia'];
1485 *
1486 * Ext.Array.each(countries, function(name, index, countriesItSelf) {
1487 * console.log(name);
1488 * });
1489 *
1490 * var sum = function() {
1491 * var sum = 0;
1492 *
1493 * Ext.Array.each(arguments, function(value) {
1494 * sum += value;
1495 * });
1496 *
1497 * return sum;
1498 * };
1499 *
1500 * sum(1, 2, 3); // returns 6
1501 *
1502 * The iteration can be stopped by returning false in the function callback.
1503 *
1504 * Ext.Array.each(countries, function(name, index, countriesItSelf) {
1505 * if (name === 'Singapore') {
1506 * return false; // break here
1507 * }
1508 * });
1509 *
1510 * {@link Ext#each Ext.each} is alias for {@link Ext.Array#each Ext.Array.each}
1511 *
1512 * @param {Array/NodeList/Object} iterable The value to be iterated. If this
1513 * argument is not iterable, the callback function is called once.
1514 * @param {Function} fn The callback function. If it returns `false`, the iteration stops and this method returns
1515 * the current `index`.
1516 * @param {Object} fn.item The item at the current `index` in the passed `array`
1517 * @param {Number} fn.index The current `index` within the `array`
1518 * @param {Array} fn.allItems The `array` itself which was passed as the first argument
1519 * @param {Boolean} fn.return Return false to stop iteration.
1520 * @param {Object} scope (Optional) The scope (`this` reference) in which the specified function is executed.
1521 * @param {Boolean} [reverse=false] (Optional) Reverse the iteration order (loop from the end to the beginning).
1522 * @return {Boolean} See description for the `fn` parameter.
1523 */
1524 each: function(array, fn, scope, reverse) {
1525 array = ExtArray.from(array);
1526
1527 var i,
1528 ln = array.length;
1529
1530 if (reverse !== true) {
1531 for (i = 0; i < ln; i++) {
1532 if (fn.call(scope || array[i], array[i], i, array) === false) {
1533 return i;
1534 }
1535 }
1536 }
1537 else {
1538 for (i = ln - 1; i > -1; i--) {
1539 if (fn.call(scope || array[i], array[i], i, array) === false) {
1540 return i;
1541 }
1542 }
1543 }
1544
1545 return true;
1546 },
1547
1548 /**
1549 * Iterates an array and invoke the given callback function for each item. Note that this will simply
1550 * delegate to the native `Array.prototype.forEach` method if supported. It doesn't support stopping the
1551 * iteration by returning `false` in the callback function like {@link Ext.Array#each}. However, performance
1552 * could be much better in modern browsers comparing with {@link Ext.Array#each}
1553 *
1554 * @param {Array} array The array to iterate.
1555 * @param {Function} fn The callback function.
1556 * @param {Object} fn.item The item at the current `index` in the passed `array`.
1557 * @param {Number} fn.index The current `index` within the `array`.
1558 * @param {Array} fn.allItems The `array` itself which was passed as the first argument.
1559 * @param {Object} scope (Optional) The execution scope (`this`) in which the specified function is executed.
1560 */
1561 forEach: supportsForEach ? function(array, fn, scope) {
1562 return array.forEach(fn, scope);
1563 } : function(array, fn, scope) {
1564 var i = 0,
1565 ln = array.length;
1566
1567 for (; i < ln; i++) {
1568 fn.call(scope, array[i], i, array);
1569 }
1570 },
1571
1572 /**
1573 * Get the index of the provided `item` in the given `array`, a supplement for the
1574 * missing arrayPrototype.indexOf in Internet Explorer.
1575 *
1576 * @param {Array} array The array to check.
1577 * @param {Object} item The item to look for.
1578 * @param {Number} from (Optional) The index at which to begin the search.
1579 * @return {Number} The index of item in the array (or -1 if it is not found).
1580 */
1581 indexOf: (supportsIndexOf) ? function(array, item, from) {
1582 return array.indexOf(item, from);
1583 } : function(array, item, from) {
1584 var i, length = array.length;
1585
1586 for (i = (from < 0) ? Math.max(0, length + from) : from || 0; i < length; i++) {
1587 if (array[i] === item) {
1588 return i;
1589 }
1590 }
1591
1592 return -1;
1593 },
1594
1595 /**
1596 * Checks whether or not the given `array` contains the specified `item`.
1597 *
1598 * @param {Array} array The array to check.
1599 * @param {Object} item The item to look for.
1600 * @return {Boolean} `true` if the array contains the item, `false` otherwise.
1601 */
1602 contains: supportsIndexOf ? function(array, item) {
1603 return array.indexOf(item) !== -1;
1604 } : function(array, item) {
1605 var i, ln;
1606
1607 for (i = 0, ln = array.length; i < ln; i++) {
1608 if (array[i] === item) {
1609 return true;
1610 }
1611 }
1612
1613 return false;
1614 },
1615
1616 /**
1617 * Converts any iterable (numeric indices and a length property) into a true array.
1618 *
1619 * function test() {
1620 * var args = Ext.Array.toArray(arguments),
1621 * fromSecondToLastArgs = Ext.Array.toArray(arguments, 1);
1622 *
1623 * alert(args.join(' '));
1624 * alert(fromSecondToLastArgs.join(' '));
1625 * }
1626 *
1627 * test('just', 'testing', 'here'); // alerts 'just testing here';
1628 * // alerts 'testing here';
1629 *
1630 * Ext.Array.toArray(document.getElementsByTagName('div')); // will convert the NodeList into an array
1631 * Ext.Array.toArray('splitted'); // returns ['s', 'p', 'l', 'i', 't', 't', 'e', 'd']
1632 * Ext.Array.toArray('splitted', 0, 3); // returns ['s', 'p', 'l', 'i']
1633 *
1634 * {@link Ext#toArray Ext.toArray} is alias for {@link Ext.Array#toArray Ext.Array.toArray}
1635 *
1636 * @param {Object} iterable the iterable object to be turned into a true Array.
1637 * @param {Number} [start=0] (Optional) a zero-based index that specifies the start of extraction.
1638 * @param {Number} [end=-1] (Optional) a zero-based index that specifies the end of extraction.
1639 * @return {Array}
1640 */
1641 toArray: function(iterable, start, end){
1642 if (!iterable || !iterable.length) {
1643 return [];
1644 }
1645
1646 if (typeof iterable === 'string') {
1647 iterable = iterable.split('');
1648 }
1649
1650 if (supportsSliceOnNodeList) {
1651 return slice.call(iterable, start || 0, end || iterable.length);
1652 }
1653
1654 var array = [],
1655 i;
1656
1657 start = start || 0;
1658 end = end ? ((end < 0) ? iterable.length + end : end) : iterable.length;
1659
1660 for (i = start; i < end; i++) {
1661 array.push(iterable[i]);
1662 }
1663
1664 return array;
1665 },
1666
1667 /**
1668 * Plucks the value of a property from each item in the Array. Example:
1669 *
1670 * Ext.Array.pluck(Ext.query("p"), "className"); // [el1.className, el2.className, ..., elN.className]
1671 *
1672 * @param {Array/NodeList} array The Array of items to pluck the value from.
1673 * @param {String} propertyName The property name to pluck from each element.
1674 * @return {Array} The value from each item in the Array.
1675 */
1676 pluck: function(array, propertyName) {
1677 var ret = [],
1678 i, ln, item;
1679
1680 for (i = 0, ln = array.length; i < ln; i++) {
1681 item = array[i];
1682
1683 ret.push(item[propertyName]);
1684 }
1685
1686 return ret;
1687 },
1688
1689 /**
1690 * Creates a new array with the results of calling a provided function on every element in this array.
1691 *
1692 * @param {Array} array
1693 * @param {Function} fn Callback function for each item.
1694 * @param {Object} scope Callback function scope.
1695 * @return {Array} results
1696 */
1697 map: supportsMap ? function(array, fn, scope) {
1698 return array.map(fn, scope);
1699 } : function(array, fn, scope) {
1700 var results = [],
1701 i = 0,
1702 len = array.length;
1703
1704 for (; i < len; i++) {
1705 results[i] = fn.call(scope, array[i], i, array);
1706 }
1707
1708 return results;
1709 },
1710
1711 /**
1712 * Executes the specified function for each array element until the function returns a falsy value.
1713 * If such an item is found, the function will return `false` immediately.
1714 * Otherwise, it will return `true`.
1715 *
1716 * @param {Array} array
1717 * @param {Function} fn Callback function for each item.
1718 * @param {Object} scope Callback function scope.
1719 * @return {Boolean} `true` if no `false` value is returned by the callback function.
1720 */
1721 every: function(array, fn, scope) {
1722 //<debug>
1723 if (!fn) {
1724 Ext.Error.raise('Ext.Array.every must have a callback function passed as second argument.');
1725 }
1726 //</debug>
1727 if (supportsEvery) {
1728 return array.every(fn, scope);
1729 }
1730
1731 var i = 0,
1732 ln = array.length;
1733
1734 for (; i < ln; ++i) {
1735 if (!fn.call(scope, array[i], i, array)) {
1736 return false;
1737 }
1738 }
1739
1740 return true;
1741 },
1742
1743 /**
1744 * Executes the specified function for each array element until the function returns a truthy value.
1745 * If such an item is found, the function will return `true` immediately. Otherwise, it will return `false`.
1746 *
1747 * @param {Array} array
1748 * @param {Function} fn Callback function for each item.
1749 * @param {Object} scope Callback function scope.
1750 * @return {Boolean} `true` if the callback function returns a truthy value.
1751 */
1752 some: function(array, fn, scope) {
1753 //<debug>
1754 if (!fn) {
1755 Ext.Error.raise('Ext.Array.some must have a callback function passed as second argument.');
1756 }
1757 //</debug>
1758 if (supportsSome) {
1759 return array.some(fn, scope);
1760 }
1761
1762 var i = 0,
1763 ln = array.length;
1764
1765 for (; i < ln; ++i) {
1766 if (fn.call(scope, array[i], i, array)) {
1767 return true;
1768 }
1769 }
1770
1771 return false;
1772 },
1773
1774 /**
1775 * Filter through an array and remove empty item as defined in {@link Ext#isEmpty Ext.isEmpty}.
1776 *
1777 * See {@link Ext.Array#filter}
1778 *
1779 * @param {Array} array
1780 * @return {Array} results
1781 */
1782 clean: function(array) {
1783 var results = [],
1784 i = 0,
1785 ln = array.length,
1786 item;
1787
1788 for (; i < ln; i++) {
1789 item = array[i];
1790
1791 if (!Ext.isEmpty(item)) {
1792 results.push(item);
1793 }
1794 }
1795
1796 return results;
1797 },
1798
1799 /**
1800 * Returns a new array with unique items.
1801 *
1802 * @param {Array} array
1803 * @return {Array} results
1804 */
1805 unique: function(array) {
1806 var clone = [],
1807 i = 0,
1808 ln = array.length,
1809 item;
1810
1811 for (; i < ln; i++) {
1812 item = array[i];
1813
1814 if (ExtArray.indexOf(clone, item) === -1) {
1815 clone.push(item);
1816 }
1817 }
1818
1819 return clone;
1820 },
1821
1822 /**
1823 * Creates a new array with all of the elements of this array for which
1824 * the provided filtering function returns `true`.
1825 *
1826 * @param {Array} array
1827 * @param {Function} fn Callback function for each item.
1828 * @param {Object} scope Callback function scope.
1829 * @return {Array} results
1830 */
1831 filter: function(array, fn, scope) {
1832 if (supportsFilter) {
1833 return array.filter(fn, scope);
1834 }
1835
1836 var results = [],
1837 i = 0,
1838 ln = array.length;
1839
1840 for (; i < ln; i++) {
1841 if (fn.call(scope, array[i], i, array)) {
1842 results.push(array[i]);
1843 }
1844 }
1845
1846 return results;
1847 },
1848
1849 /**
1850 * Converts a value to an array if it's not already an array; returns:
1851 *
1852 * - An empty array if given value is `undefined` or `null`
1853 * - Itself if given value is already an array
1854 * - An array copy if given value is {@link Ext#isIterable iterable} (arguments, NodeList and alike)
1855 * - An array with one item which is the given value, otherwise
1856 *
1857 * @param {Object} value The value to convert to an array if it's not already is an array.
1858 * @param {Boolean} [newReference=false] (Optional) `true` to clone the given array and return a new reference if necessary.
1859 * @return {Array} array
1860 */
1861 from: function(value, newReference) {
1862 if (value === undefined || value === null) {
1863 return [];
1864 }
1865
1866 if (Ext.isArray(value)) {
1867 return (newReference) ? slice.call(value) : value;
1868 }
1869
1870 if (value && value.length !== undefined && typeof value !== 'string') {
1871 return ExtArray.toArray(value);
1872 }
1873
1874 return [value];
1875 },
1876
1877 /**
1878 * Removes the specified item from the array if it exists.
1879 *
1880 * @param {Array} array The array.
1881 * @param {Object} item The item to remove.
1882 * @return {Array} The passed array itself.
1883 */
1884 remove: function(array, item) {
1885 var index = ExtArray.indexOf(array, item);
1886
1887 if (index !== -1) {
1888 erase(array, index, 1);
1889 }
1890
1891 return array;
1892 },
1893
1894 /**
1895 * Push an item into the array only if the array doesn't contain it yet.
1896 *
1897 * @param {Array} array The array.
1898 * @param {Object} item The item to include.
1899 */
1900 include: function(array, item) {
1901 if (!ExtArray.contains(array, item)) {
1902 array.push(item);
1903 }
1904 },
1905
1906 /**
1907 * Clone a flat array without referencing the previous one. Note that this is different
1908 * from `Ext.clone` since it doesn't handle recursive cloning. It's simply a convenient, easy-to-remember method
1909 * for `Array.prototype.slice.call(array)`.
1910 *
1911 * @param {Array} array The array
1912 * @return {Array} The clone array
1913 */
1914 clone: function(array) {
1915 return slice.call(array);
1916 },
1917
1918 /**
1919 * Merge multiple arrays into one with unique items.
1920 *
1921 * {@link Ext.Array#union} is alias for {@link Ext.Array#merge}
1922 *
1923 * @param {Array} array1
1924 * @param {Array} array2
1925 * @param {Array} etc
1926 * @return {Array} merged
1927 */
1928 merge: function() {
1929 var args = slice.call(arguments),
1930 array = [],
1931 i, ln;
1932
1933 for (i = 0, ln = args.length; i < ln; i++) {
1934 array = array.concat(args[i]);
1935 }
1936
1937 return ExtArray.unique(array);
1938 },
1939
1940 /**
1941 * Merge multiple arrays into one with unique items that exist in all of the arrays.
1942 *
1943 * @param {Array} array1
1944 * @param {Array} array2
1945 * @param {Array} etc
1946 * @return {Array} intersect
1947 */
1948 intersect: function() {
1949 var intersect = [],
1950 arrays = slice.call(arguments),
1951 item, minArray, itemIndex, arrayIndex;
1952
1953 if (!arrays.length) {
1954 return intersect;
1955 }
1956
1957 //Find the Smallest Array
1958 arrays = arrays.sort(function(a, b) {
1959 if (a.length > b.length) {
1960 return 1;
1961 } else if (a.length < b.length) {
1962 return -1;
1963 } else {
1964 return 0;
1965 }
1966 });
1967
1968 //Remove duplicates from smallest array
1969 minArray = ExtArray.unique(arrays[0]);
1970
1971 //Populate intersecting values
1972 for (itemIndex = 0; itemIndex < minArray.length; itemIndex++) {
1973 item = minArray[itemIndex];
1974 for (arrayIndex = 1; arrayIndex < arrays.length; arrayIndex++) {
1975 if (arrays[arrayIndex].indexOf(item) === -1) {
1976 break;
1977 }
1978
1979 if (arrayIndex == (arrays.length - 1)) {
1980 intersect.push(item);
1981 }
1982 }
1983 }
1984
1985 return intersect;
1986 },
1987
1988 /**
1989 * Perform a set difference A-B by subtracting all items in array B from array A.
1990 *
1991 * @param {Array} arrayA
1992 * @param {Array} arrayB
1993 * @return {Array} difference
1994 */
1995 difference: function(arrayA, arrayB) {
1996 var clone = slice.call(arrayA),
1997 ln = clone.length,
1998 i, j, lnB;
1999
2000 for (i = 0,lnB = arrayB.length; i < lnB; i++) {
2001 for (j = 0; j < ln; j++) {
2002 if (clone[j] === arrayB[i]) {
2003 erase(clone, j, 1);
2004 j--;
2005 ln--;
2006 }
2007 }
2008 }
2009
2010 return clone;
2011 },
2012
2013 /**
2014 * Returns a shallow copy of a part of an array. This is equivalent to the native
2015 * call `Array.prototype.slice.call(array, begin, end)`. This is often used when "array"
2016 * is "arguments" since the arguments object does not supply a slice method but can
2017 * be the context object to `Array.prototype.slice()`.
2018 *
2019 * @param {Array} array The array (or arguments object).
2020 * @param {Number} begin The index at which to begin. Negative values are offsets from
2021 * the end of the array.
2022 * @param {Number} end The index at which to end. The copied items do not include
2023 * end. Negative values are offsets from the end of the array. If end is omitted,
2024 * all items up to the end of the array are copied.
2025 * @return {Array} The copied piece of the array.
2026 */
2027 slice: function(array, begin, end) {
2028 return slice.call(array, begin, end);
2029 },
2030
2031 /**
2032 * Sorts the elements of an Array.
2033 * By default, this method sorts the elements alphabetically and ascending.
2034 *
2035 * @param {Array} array The array to sort.
2036 * @param {Function} sortFn (optional) The comparison function.
2037 * @return {Array} The sorted array.
2038 */
2039 sort: function(array, sortFn) {
2040 if (supportsSort) {
2041 if (sortFn) {
2042 return array.sort(sortFn);
2043 } else {
2044 return array.sort();
2045 }
2046 }
2047
2048 var length = array.length,
2049 i = 0,
2050 comparison,
2051 j, min, tmp;
2052
2053 for (; i < length; i++) {
2054 min = i;
2055 for (j = i + 1; j < length; j++) {
2056 if (sortFn) {
2057 comparison = sortFn(array[j], array[min]);
2058 if (comparison < 0) {
2059 min = j;
2060 }
2061 } else if (array[j] < array[min]) {
2062 min = j;
2063 }
2064 }
2065 if (min !== i) {
2066 tmp = array[i];
2067 array[i] = array[min];
2068 array[min] = tmp;
2069 }
2070 }
2071
2072 return array;
2073 },
2074
2075 /**
2076 * Recursively flattens into 1-d Array. Injects Arrays inline.
2077 *
2078 * @param {Array} array The array to flatten
2079 * @return {Array} The 1-d array.
2080 */
2081 flatten: function(array) {
2082 var worker = [];
2083
2084 function rFlatten(a) {
2085 var i, ln, v;
2086
2087 for (i = 0, ln = a.length; i < ln; i++) {
2088 v = a[i];
2089
2090 if (Ext.isArray(v)) {
2091 rFlatten(v);
2092 } else {
2093 worker.push(v);
2094 }
2095 }
2096
2097 return worker;
2098 }
2099
2100 return rFlatten(array);
2101 },
2102
2103 /**
2104 * Returns the minimum value in the Array.
2105 *
2106 * @param {Array/NodeList} array The Array from which to select the minimum value.
2107 * @param {Function} comparisonFn (optional) a function to perform the comparison which determines minimization.
2108 * If omitted the "<" operator will be used.
2109 * __Note:__ gt = 1; eq = 0; lt = -1
2110 * @return {Object} minValue The minimum value.
2111 */
2112 min: function(array, comparisonFn) {
2113 var min = array[0],
2114 i, ln, item;
2115
2116 for (i = 0, ln = array.length; i < ln; i++) {
2117 item = array[i];
2118
2119 if (comparisonFn) {
2120 if (comparisonFn(min, item) === 1) {
2121 min = item;
2122 }
2123 }
2124 else {
2125 if (item < min) {
2126 min = item;
2127 }
2128 }
2129 }
2130
2131 return min;
2132 },
2133
2134 /**
2135 * Returns the maximum value in the Array.
2136 *
2137 * @param {Array/NodeList} array The Array from which to select the maximum value.
2138 * @param {Function} comparisonFn (optional) a function to perform the comparison which determines maximization.
2139 * If omitted the ">" operator will be used.
2140 * __Note:__ gt = 1; eq = 0; lt = -1
2141 * @return {Object} maxValue The maximum value
2142 */
2143 max: function(array, comparisonFn) {
2144 var max = array[0],
2145 i, ln, item;
2146
2147 for (i = 0, ln = array.length; i < ln; i++) {
2148 item = array[i];
2149
2150 if (comparisonFn) {
2151 if (comparisonFn(max, item) === -1) {
2152 max = item;
2153 }
2154 }
2155 else {
2156 if (item > max) {
2157 max = item;
2158 }
2159 }
2160 }
2161
2162 return max;
2163 },
2164
2165 /**
2166 * Calculates the mean of all items in the array.
2167 *
2168 * @param {Array} array The Array to calculate the mean value of.
2169 * @return {Number} The mean.
2170 */
2171 mean: function(array) {
2172 return array.length > 0 ? ExtArray.sum(array) / array.length : undefined;
2173 },
2174
2175 /**
2176 * Calculates the sum of all items in the given array.
2177 *
2178 * @param {Array} array The Array to calculate the sum value of.
2179 * @return {Number} The sum.
2180 */
2181 sum: function(array) {
2182 var sum = 0,
2183 i, ln, item;
2184
2185 for (i = 0,ln = array.length; i < ln; i++) {
2186 item = array[i];
2187
2188 sum += item;
2189 }
2190
2191 return sum;
2192 },
2193
2194 //<debug>
2195 _replaceSim: replaceSim, // for unit testing
2196 _spliceSim: spliceSim,
2197 //</debug>
2198
2199 /**
2200 * Removes items from an array. This is functionally equivalent to the splice method
2201 * of Array, but works around bugs in IE8's splice method and does not copy the
2202 * removed elements in order to return them (because very often they are ignored).
2203 *
2204 * @param {Array} array The Array on which to replace.
2205 * @param {Number} index The index in the array at which to operate.
2206 * @param {Number} removeCount The number of items to remove at index.
2207 * @return {Array} The array passed.
2208 * @method
2209 */
2210 erase: erase,
2211
2212 /**
2213 * Inserts items in to an array.
2214 *
2215 * @param {Array} array The Array on which to replace.
2216 * @param {Number} index The index in the array at which to operate.
2217 * @param {Array} items The array of items to insert at index.
2218 * @return {Array} The array passed.
2219 */
2220 insert: function (array, index, items) {
2221 return replace(array, index, 0, items);
2222 },
2223
2224 /**
2225 * Replaces items in an array. This is functionally equivalent to the splice method
2226 * of Array, but works around bugs in IE8's splice method and is often more convenient
2227 * to call because it accepts an array of items to insert rather than use a variadic
2228 * argument list.
2229 *
2230 * @param {Array} array The Array on which to replace.
2231 * @param {Number} index The index in the array at which to operate.
2232 * @param {Number} removeCount The number of items to remove at index (can be 0).
2233 * @param {Array} insert (optional) An array of items to insert at index.
2234 * @return {Array} The array passed.
2235 * @method
2236 */
2237 replace: replace,
2238
2239 /**
2240 * Replaces items in an array. This is equivalent to the splice method of Array, but
2241 * works around bugs in IE8's splice method. The signature is exactly the same as the
2242 * splice method except that the array is the first argument. All arguments following
2243 * removeCount are inserted in the array at index.
2244 *
2245 * @param {Array} array The Array on which to replace.
2246 * @param {Number} index The index in the array at which to operate.
2247 * @param {Number} removeCount The number of items to remove at index (can be 0).
2248 * @return {Array} An array containing the removed items.
2249 * @method
2250 */
2251 splice: splice
2252 };
2253
2254 /**
2255 * @method
2256 * @member Ext
2257 * @alias Ext.Array#each
2258 */
2259 Ext.each = ExtArray.each;
2260
2261 /**
2262 * @method
2263 * @member Ext.Array
2264 * @alias Ext.Array#merge
2265 */
2266 ExtArray.union = ExtArray.merge;
2267
2268 /**
2269 * Old alias to {@link Ext.Array#min}
2270 * @deprecated 2.0.0 Please use {@link Ext.Array#min} instead
2271 * @method
2272 * @member Ext
2273 * @alias Ext.Array#min
2274 */
2275 Ext.min = ExtArray.min;
2276
2277 /**
2278 * Old alias to {@link Ext.Array#max}
2279 * @deprecated 2.0.0 Please use {@link Ext.Array#max} instead
2280 * @method
2281 * @member Ext
2282 * @alias Ext.Array#max
2283 */
2284 Ext.max = ExtArray.max;
2285
2286 /**
2287 * Old alias to {@link Ext.Array#sum}
2288 * @deprecated 2.0.0 Please use {@link Ext.Array#sum} instead
2289 * @method
2290 * @member Ext
2291 * @alias Ext.Array#sum
2292 */
2293 Ext.sum = ExtArray.sum;
2294
2295 /**
2296 * Old alias to {@link Ext.Array#mean}
2297 * @deprecated 2.0.0 Please use {@link Ext.Array#mean} instead
2298 * @method
2299 * @member Ext
2300 * @alias Ext.Array#mean
2301 */
2302 Ext.mean = ExtArray.mean;
2303
2304 /**
2305 * Old alias to {@link Ext.Array#flatten}
2306 * @deprecated 2.0.0 Please use {@link Ext.Array#flatten} instead
2307 * @method
2308 * @member Ext
2309 * @alias Ext.Array#flatten
2310 */
2311 Ext.flatten = ExtArray.flatten;
2312
2313 /**
2314 * Old alias to {@link Ext.Array#clean}
2315 * @deprecated 2.0.0 Please use {@link Ext.Array#clean} instead
2316 * @method
2317 * @member Ext
2318 * @alias Ext.Array#clean
2319 */
2320 Ext.clean = ExtArray.clean;
2321
2322 /**
2323 * Old alias to {@link Ext.Array#unique}
2324 * @deprecated 2.0.0 Please use {@link Ext.Array#unique} instead
2325 * @method
2326 * @member Ext
2327 * @alias Ext.Array#unique
2328 */
2329 Ext.unique = ExtArray.unique;
2330
2331 /**
2332 * Old alias to {@link Ext.Array#pluck Ext.Array.pluck}
2333 * @deprecated 2.0.0 Please use {@link Ext.Array#pluck Ext.Array.pluck} instead
2334 * @method
2335 * @member Ext
2336 * @alias Ext.Array#pluck
2337 */
2338 Ext.pluck = ExtArray.pluck;
2339
2340 /**
2341 * @method
2342 * @member Ext
2343 * @alias Ext.Array#toArray
2344 */
2345 Ext.toArray = function() {
2346 return ExtArray.toArray.apply(ExtArray, arguments);
2347 };
2348 })();
2349
2350 //@tag foundation,core
2351 //@define Ext.Number
2352 //@require Ext.Array
2353
2354 /**
2355 * @class Ext.Number
2356 *
2357 * A collection of useful static methods to deal with numbers
2358 * @singleton
2359 */
2360
2361 (function() {
2362
2363 var isToFixedBroken = (0.9).toFixed() !== '1';
2364
2365 Ext.Number = {
2366 /**
2367 * Checks whether or not the passed number is within a desired range. If the number is already within the
2368 * range it is returned, otherwise the min or max value is returned depending on which side of the range is
2369 * exceeded. Note that this method returns the constrained value but does not change the current number.
2370 * @param {Number} number The number to check
2371 * @param {Number} min The minimum number in the range
2372 * @param {Number} max The maximum number in the range
2373 * @return {Number} The constrained value if outside the range, otherwise the current value
2374 */
2375 constrain: function(number, min, max) {
2376 number = parseFloat(number);
2377
2378 if (!isNaN(min)) {
2379 number = Math.max(number, min);
2380 }
2381 if (!isNaN(max)) {
2382 number = Math.min(number, max);
2383 }
2384 return number;
2385 },
2386
2387 /**
2388 * Snaps the passed number between stopping points based upon a passed increment value.
2389 * @param {Number} value The unsnapped value.
2390 * @param {Number} increment The increment by which the value must move.
2391 * @param {Number} minValue The minimum value to which the returned value must be constrained. Overrides the increment..
2392 * @param {Number} maxValue The maximum value to which the returned value must be constrained. Overrides the increment..
2393 * @return {Number} The value of the nearest snap target.
2394 */
2395 snap : function(value, increment, minValue, maxValue) {
2396 var newValue = value,
2397 m;
2398
2399 if (!(increment && value)) {
2400 return value;
2401 }
2402 m = value % increment;
2403 if (m !== 0) {
2404 newValue -= m;
2405 if (m * 2 >= increment) {
2406 newValue += increment;
2407 } else if (m * 2 < -increment) {
2408 newValue -= increment;
2409 }
2410 }
2411 return Ext.Number.constrain(newValue, minValue, maxValue);
2412 },
2413
2414 /**
2415 * Formats a number using fixed-point notation
2416 * @param {Number} value The number to format
2417 * @param {Number} precision The number of digits to show after the decimal point
2418 */
2419 toFixed: function(value, precision) {
2420 if (isToFixedBroken) {
2421 precision = precision || 0;
2422 var pow = Math.pow(10, precision);
2423 return (Math.round(value * pow) / pow).toFixed(precision);
2424 }
2425
2426 return value.toFixed(precision);
2427 },
2428
2429 /**
2430 * Validate that a value is numeric and convert it to a number if necessary. Returns the specified default value if
2431 * it is not.
2432
2433 Ext.Number.from('1.23', 1); // returns 1.23
2434 Ext.Number.from('abc', 1); // returns 1
2435
2436 * @param {Object} value
2437 * @param {Number} defaultValue The value to return if the original value is non-numeric
2438 * @return {Number} value, if numeric, defaultValue otherwise
2439 */
2440 from: function(value, defaultValue) {
2441 if (isFinite(value)) {
2442 value = parseFloat(value);
2443 }
2444
2445 return !isNaN(value) ? value : defaultValue;
2446 }
2447 };
2448
2449 })();
2450
2451 /**
2452 * This method is deprecated, please use {@link Ext.Number#from Ext.Number.from} instead
2453 *
2454 * @deprecated 2.0.0 Replaced by Ext.Number.from
2455 * @member Ext
2456 * @method num
2457 */
2458 Ext.num = function() {
2459 return Ext.Number.from.apply(this, arguments);
2460 };
2461
2462 //@tag foundation,core
2463 //@define Ext.Object
2464 //@require Ext.Number
2465
2466 /**
2467 * @author Jacky Nguyen <jacky@sencha.com>
2468 * @docauthor Jacky Nguyen <jacky@sencha.com>
2469 * @class Ext.Object
2470 *
2471 * A collection of useful static methods to deal with objects.
2472 *
2473 * @singleton
2474 */
2475
2476 (function() {
2477
2478 // The "constructor" for chain:
2479 var TemplateClass = function(){};
2480
2481 var ExtObject = Ext.Object = {
2482
2483 /**
2484 * Returns a new object with the given object as the prototype chain.
2485 * @param {Object} object The prototype chain for the new object.
2486 */
2487 chain: ('create' in Object) ? function(object){
2488 return Object.create(object);
2489 } : function (object) {
2490 TemplateClass.prototype = object;
2491 var result = new TemplateClass();
2492 TemplateClass.prototype = null;
2493 return result;
2494 },
2495
2496 /**
2497 * Convert a `name` - `value` pair to an array of objects with support for nested structures; useful to construct
2498 * query strings. For example:
2499 *
2500 * Non-recursive:
2501 *
2502 * var objects = Ext.Object.toQueryObjects('hobbies', ['reading', 'cooking', 'swimming']);
2503 *
2504 * // objects then equals:
2505 * [
2506 * { name: 'hobbies', value: 'reading' },
2507 * { name: 'hobbies', value: 'cooking' },
2508 * { name: 'hobbies', value: 'swimming' }
2509 * ]
2510 *
2511 * Recursive:
2512 *
2513 * var objects = Ext.Object.toQueryObjects('dateOfBirth', {
2514 * day: 3,
2515 * month: 8,
2516 * year: 1987,
2517 * extra: {
2518 * hour: 4,
2519 * minute: 30
2520 * }
2521 * }, true);
2522 *
2523 * // objects then equals:
2524 * [
2525 * { name: 'dateOfBirth[day]', value: 3 },
2526 * { name: 'dateOfBirth[month]', value: 8 },
2527 * { name: 'dateOfBirth[year]', value: 1987 },
2528 * { name: 'dateOfBirth[extra][hour]', value: 4 },
2529 * { name: 'dateOfBirth[extra][minute]', value: 30 }
2530 * ]
2531 *
2532 * @param {String} name
2533 * @param {Object} value
2534 * @param {Boolean} [recursive=false] `true` to recursively encode any sub-objects.
2535 * @return {Object[]} Array of objects with `name` and `value` fields.
2536 */
2537 toQueryObjects: function(name, value, recursive) {
2538 var self = ExtObject.toQueryObjects,
2539 objects = [],
2540 i, ln;
2541
2542 if (Ext.isArray(value)) {
2543 for (i = 0, ln = value.length; i < ln; i++) {
2544 if (recursive) {
2545 objects = objects.concat(self(name + '[' + i + ']', value[i], true));
2546 }
2547 else {
2548 objects.push({
2549 name: name,
2550 value: value[i]
2551 });
2552 }
2553 }
2554 }
2555 else if (Ext.isObject(value)) {
2556 for (i in value) {
2557 if (value.hasOwnProperty(i)) {
2558 if (recursive) {
2559 objects = objects.concat(self(name + '[' + i + ']', value[i], true));
2560 }
2561 else {
2562 objects.push({
2563 name: name,
2564 value: value[i]
2565 });
2566 }
2567 }
2568 }
2569 }
2570 else {
2571 objects.push({
2572 name: name,
2573 value: value
2574 });
2575 }
2576
2577 return objects;
2578 },
2579
2580 /**
2581 * Takes an object and converts it to an encoded query string.
2582 *
2583 * Non-recursive:
2584 *
2585 * Ext.Object.toQueryString({foo: 1, bar: 2}); // returns "foo=1&bar=2"
2586 * Ext.Object.toQueryString({foo: null, bar: 2}); // returns "foo=&bar=2"
2587 * Ext.Object.toQueryString({'some price': '$300'}); // returns "some%20price=%24300"
2588 * Ext.Object.toQueryString({date: new Date(2011, 0, 1)}); // returns "date=%222011-01-01T00%3A00%3A00%22"
2589 * Ext.Object.toQueryString({colors: ['red', 'green', 'blue']}); // returns "colors=red&colors=green&colors=blue"
2590 *
2591 * Recursive:
2592 *
2593 * Ext.Object.toQueryString({
2594 * username: 'Jacky',
2595 * dateOfBirth: {
2596 * day: 1,
2597 * month: 2,
2598 * year: 1911
2599 * },
2600 * hobbies: ['coding', 'eating', 'sleeping', ['nested', 'stuff']]
2601 * }, true);
2602 *
2603 * // returns the following string (broken down and url-decoded for ease of reading purpose):
2604 * // username=Jacky
2605 * // &dateOfBirth[day]=1&dateOfBirth[month]=2&dateOfBirth[year]=1911
2606 * // &hobbies[0]=coding&hobbies[1]=eating&hobbies[2]=sleeping&hobbies[3][0]=nested&hobbies[3][1]=stuff
2607 *
2608 * @param {Object} object The object to encode.
2609 * @param {Boolean} [recursive=false] Whether or not to interpret the object in recursive format.
2610 * (PHP / Ruby on Rails servers and similar).
2611 * @return {String} queryString
2612 */
2613 toQueryString: function(object, recursive) {
2614 var paramObjects = [],
2615 params = [],
2616 i, j, ln, paramObject, value;
2617
2618 for (i in object) {
2619 if (object.hasOwnProperty(i)) {
2620 paramObjects = paramObjects.concat(ExtObject.toQueryObjects(i, object[i], recursive));
2621 }
2622 }
2623
2624 for (j = 0, ln = paramObjects.length; j < ln; j++) {
2625 paramObject = paramObjects[j];
2626 value = paramObject.value;
2627
2628 if (Ext.isEmpty(value)) {
2629 value = '';
2630 }
2631 else if (Ext.isDate(value)) {
2632 value = Ext.Date.toString(value);
2633 }
2634
2635 params.push(encodeURIComponent(paramObject.name) + '=' + encodeURIComponent(String(value)));
2636 }
2637
2638 return params.join('&');
2639 },
2640
2641 /**
2642 * Converts a query string back into an object.
2643 *
2644 * Non-recursive:
2645 *
2646 * Ext.Object.fromQueryString("foo=1&bar=2"); // returns {foo: 1, bar: 2}
2647 * Ext.Object.fromQueryString("foo=&bar=2"); // returns {foo: null, bar: 2}
2648 * Ext.Object.fromQueryString("some%20price=%24300"); // returns {'some price': '$300'}
2649 * Ext.Object.fromQueryString("colors=red&colors=green&colors=blue"); // returns {colors: ['red', 'green', 'blue']}
2650 *
2651 * Recursive:
2652 *
2653 * Ext.Object.fromQueryString("username=Jacky&dateOfBirth[day]=1&dateOfBirth[month]=2&dateOfBirth[year]=1911&hobbies[0]=coding&hobbies[1]=eating&hobbies[2]=sleeping&hobbies[3][0]=nested&hobbies[3][1]=stuff", true);
2654 *
2655 * // returns
2656 * {
2657 * username: 'Jacky',
2658 * dateOfBirth: {
2659 * day: '1',
2660 * month: '2',
2661 * year: '1911'
2662 * },
2663 * hobbies: ['coding', 'eating', 'sleeping', ['nested', 'stuff']]
2664 * }
2665 *
2666 * @param {String} queryString The query string to decode.
2667 * @param {Boolean} [recursive=false] Whether or not to recursively decode the string. This format is supported by
2668 * PHP / Ruby on Rails servers and similar.
2669 * @return {Object}
2670 */
2671 fromQueryString: function(queryString, recursive) {
2672 var parts = queryString.replace(/^\?/, '').split('&'),
2673 object = {},
2674 temp, components, name, value, i, ln,
2675 part, j, subLn, matchedKeys, matchedName,
2676 keys, key, nextKey;
2677
2678 for (i = 0, ln = parts.length; i < ln; i++) {
2679 part = parts[i];
2680
2681 if (part.length > 0) {
2682 components = part.split('=');
2683 name = decodeURIComponent(components[0]);
2684 value = (components[1] !== undefined) ? decodeURIComponent(components[1]) : '';
2685
2686 if (!recursive) {
2687 if (object.hasOwnProperty(name)) {
2688 if (!Ext.isArray(object[name])) {
2689 object[name] = [object[name]];
2690 }
2691
2692 object[name].push(value);
2693 }
2694 else {
2695 object[name] = value;
2696 }
2697 }
2698 else {
2699 matchedKeys = name.match(/(\[):?([^\]]*)\]/g);
2700 matchedName = name.match(/^([^\[]+)/);
2701
2702 //<debug error>
2703 if (!matchedName) {
2704 throw new Error('[Ext.Object.fromQueryString] Malformed query string given, failed parsing name from "' + part + '"');
2705 }
2706 //</debug>
2707
2708 name = matchedName[0];
2709 keys = [];
2710
2711 if (matchedKeys === null) {
2712 object[name] = value;
2713 continue;
2714 }
2715
2716 for (j = 0, subLn = matchedKeys.length; j < subLn; j++) {
2717 key = matchedKeys[j];
2718 key = (key.length === 2) ? '' : key.substring(1, key.length - 1);
2719 keys.push(key);
2720 }
2721
2722 keys.unshift(name);
2723
2724 temp = object;
2725
2726 for (j = 0, subLn = keys.length; j < subLn; j++) {
2727 key = keys[j];
2728
2729 if (j === subLn - 1) {
2730 if (Ext.isArray(temp) && key === '') {
2731 temp.push(value);
2732 }
2733 else {
2734 temp[key] = value;
2735 }
2736 }
2737 else {
2738 if (temp[key] === undefined || typeof temp[key] === 'string') {
2739 nextKey = keys[j+1];
2740
2741 temp[key] = (Ext.isNumeric(nextKey) || nextKey === '') ? [] : {};
2742 }
2743
2744 temp = temp[key];
2745 }
2746 }
2747 }
2748 }
2749 }
2750
2751 return object;
2752 },
2753
2754 /**
2755 * Iterate through an object and invoke the given callback function for each iteration. The iteration can be stop
2756 * by returning `false` in the callback function. This method iterates over properties within the current object,
2757 * not properties from its prototype. To iterate over a prototype, iterate over obj.proto instead of obj.
2758 * In the next example, use Ext.Object.each(Person.proto ....) and so on.
2759 * For example:
2760 *
2761 * var person = {
2762 * name: 'Jacky',
2763 * hairColor: 'black',
2764 * loves: ['food', 'sleeping', 'wife']
2765 * };
2766 *
2767 * Ext.Object.each(person, function(key, value, myself) {
2768 * console.log(key + ":" + value);
2769 *
2770 * if (key === 'hairColor') {
2771 * return false; // stop the iteration
2772 * }
2773 * });
2774 *
2775 * @param {Object} object The object to iterate
2776 * @param {Function} fn The callback function.
2777 * @param {String} fn.key
2778 * @param {Mixed} fn.value
2779 * @param {Object} fn.object The object itself
2780 * @param {Object} [scope] The execution scope (`this`) of the callback function
2781 */
2782 each: function(object, fn, scope) {
2783 for (var property in object) {
2784 if (object.hasOwnProperty(property)) {
2785 if (fn.call(scope || object, property, object[property], object) === false) {
2786 return;
2787 }
2788 }
2789 }
2790 },
2791
2792 /**
2793 * Merges any number of objects recursively without referencing them or their children.
2794 *
2795 * var extjs = {
2796 * companyName: 'Ext JS',
2797 * products: ['Ext JS', 'Ext GWT', 'Ext Designer'],
2798 * isSuperCool: true,
2799 * office: {
2800 * size: 2000,
2801 * location: 'Palo Alto',
2802 * isFun: true
2803 * }
2804 * };
2805 *
2806 * var newStuff = {
2807 * companyName: 'Sencha Inc.',
2808 * products: ['Ext JS', 'Ext GWT', 'Ext Designer', 'Sencha Touch', 'Sencha Animator'],
2809 * office: {
2810 * size: 40000,
2811 * location: 'Redwood City'
2812 * }
2813 * };
2814 *
2815 * var sencha = Ext.Object.merge({}, extjs, newStuff);
2816 *
2817 * // sencha then equals to
2818 * {
2819 * companyName: 'Sencha Inc.',
2820 * products: ['Ext JS', 'Ext GWT', 'Ext Designer', 'Sencha Touch', 'Sencha Animator'],
2821 * isSuperCool: true
2822 * office: {
2823 * size: 40000,
2824 * location: 'Redwood City'
2825 * isFun: true
2826 * }
2827 * }
2828 *
2829 * @param {Object} source The first object into which to merge the others.
2830 * @param {Object...} objs One or more objects to be merged into the first.
2831 * @return {Object} The object that is created as a result of merging all the objects passed in.
2832 */
2833 merge: function(source) {
2834 var i = 1,
2835 ln = arguments.length,
2836 mergeFn = ExtObject.merge,
2837 cloneFn = Ext.clone,
2838 object, key, value, sourceKey;
2839
2840 for (; i < ln; i++) {
2841 object = arguments[i];
2842
2843 for (key in object) {
2844 value = object[key];
2845 if (value && value.constructor === Object) {
2846 sourceKey = source[key];
2847 if (sourceKey && sourceKey.constructor === Object) {
2848 mergeFn(sourceKey, value);
2849 }
2850 else {
2851 source[key] = cloneFn(value);
2852 }
2853 }
2854 else {
2855 source[key] = value;
2856 }
2857 }
2858 }
2859
2860 return source;
2861 },
2862
2863 /**
2864 * @param {Object} source
2865 */
2866 mergeIf: function(source) {
2867 var i = 1,
2868 ln = arguments.length,
2869 cloneFn = Ext.clone,
2870 object, key, value;
2871
2872 for (; i < ln; i++) {
2873 object = arguments[i];
2874
2875 for (key in object) {
2876 if (!(key in source)) {
2877 value = object[key];
2878
2879 if (value && value.constructor === Object) {
2880 source[key] = cloneFn(value);
2881 }
2882 else {
2883 source[key] = value;
2884 }
2885 }
2886 }
2887 }
2888
2889 return source;
2890 },
2891
2892 /**
2893 * Returns the first matching key corresponding to the given value.
2894 * If no matching value is found, `null` is returned.
2895 *
2896 * var person = {
2897 * name: 'Jacky',
2898 * loves: 'food'
2899 * };
2900 *
2901 * alert(Ext.Object.getKey(sencha, 'food')); // alerts 'loves'
2902 *
2903 * @param {Object} object
2904 * @param {Object} value The value to find
2905 */
2906 getKey: function(object, value) {
2907 for (var property in object) {
2908 if (object.hasOwnProperty(property) && object[property] === value) {
2909 return property;
2910 }
2911 }
2912
2913 return null;
2914 },
2915
2916 /**
2917 * Gets all values of the given object as an array.
2918 *
2919 * var values = Ext.Object.getValues({
2920 * name: 'Jacky',
2921 * loves: 'food'
2922 * }); // ['Jacky', 'food']
2923 *
2924 * @param {Object} object
2925 * @return {Array} An array of values from the object.
2926 */
2927 getValues: function(object) {
2928 var values = [],
2929 property;
2930
2931 for (property in object) {
2932 if (object.hasOwnProperty(property)) {
2933 values.push(object[property]);
2934 }
2935 }
2936
2937 return values;
2938 },
2939
2940 /**
2941 * Gets all keys of the given object as an array.
2942 *
2943 * var values = Ext.Object.getKeys({
2944 * name: 'Jacky',
2945 * loves: 'food'
2946 * }); // ['name', 'loves']
2947 *
2948 * @param {Object} object
2949 * @return {String[]} An array of keys from the object.
2950 * @method
2951 */
2952 getKeys: ('keys' in Object) ? Object.keys : function(object) {
2953 var keys = [],
2954 property;
2955
2956 for (property in object) {
2957 if (object.hasOwnProperty(property)) {
2958 keys.push(property);
2959 }
2960 }
2961
2962 return keys;
2963 },
2964
2965 /**
2966 * Gets the total number of this object's own properties.
2967 *
2968 * var size = Ext.Object.getSize({
2969 * name: 'Jacky',
2970 * loves: 'food'
2971 * }); // size equals 2
2972 *
2973 * @param {Object} object
2974 * @return {Number} size
2975 */
2976 getSize: function(object) {
2977 var size = 0,
2978 property;
2979
2980 for (property in object) {
2981 if (object.hasOwnProperty(property)) {
2982 size++;
2983 }
2984 }
2985
2986 return size;
2987 },
2988
2989 /**
2990 * @private
2991 */
2992 classify: function(object) {
2993 var objectProperties = [],
2994 arrayProperties = [],
2995 propertyClassesMap = {},
2996 objectClass = function() {
2997 var i = 0,
2998 ln = objectProperties.length,
2999 property;
3000
3001 for (; i < ln; i++) {
3002 property = objectProperties[i];
3003 this[property] = new propertyClassesMap[property];
3004 }
3005
3006 ln = arrayProperties.length;
3007
3008 for (i = 0; i < ln; i++) {
3009 property = arrayProperties[i];
3010 this[property] = object[property].slice();
3011 }
3012 },
3013 key, value, constructor;
3014
3015 for (key in object) {
3016 if (object.hasOwnProperty(key)) {
3017 value = object[key];
3018
3019 if (value) {
3020 constructor = value.constructor;
3021
3022 if (constructor === Object) {
3023 objectProperties.push(key);
3024 propertyClassesMap[key] = ExtObject.classify(value);
3025 }
3026 else if (constructor === Array) {
3027 arrayProperties.push(key);
3028 }
3029 }
3030 }
3031 }
3032
3033 objectClass.prototype = object;
3034
3035 return objectClass;
3036 },
3037
3038 equals: function(origin, target) {
3039 var originType = typeof origin,
3040 targetType = typeof target,
3041 key;
3042
3043 if (targetType === targetType) {
3044 if (originType === 'object') {
3045 for (key in origin) {
3046 if (!(key in target)) {
3047 return false;
3048 }
3049
3050 if (!ExtObject.equals(origin[key], target[key])) {
3051 return false;
3052 }
3053 }
3054
3055 for (key in target) {
3056 if (!(key in origin)) {
3057 return false;
3058 }
3059 }
3060
3061 return true;
3062 }
3063 else {
3064 return origin === target;
3065 }
3066 }
3067
3068 return false;
3069 },
3070
3071 defineProperty: ('defineProperty' in Object) ? Object.defineProperty : function(object, name, descriptor) {
3072 if (descriptor.get) {
3073 object.__defineGetter__(name, descriptor.get);
3074 }
3075
3076 if (descriptor.set) {
3077 object.__defineSetter__(name, descriptor.set);
3078 }
3079 }
3080 };
3081
3082 /**
3083 * A convenient alias method for {@link Ext.Object#merge}.
3084 *
3085 * @member Ext
3086 * @method merge
3087 */
3088 Ext.merge = Ext.Object.merge;
3089
3090 /**
3091 * @private
3092 */
3093 Ext.mergeIf = Ext.Object.mergeIf;
3094
3095 /**
3096 * A convenient alias method for {@link Ext.Object#toQueryString}.
3097 *
3098 * @member Ext
3099 * @method urlEncode
3100 * @deprecated 2.0.0 Please use `{@link Ext.Object#toQueryString Ext.Object.toQueryString}` instead
3101 */
3102 Ext.urlEncode = function() {
3103 var args = Ext.Array.from(arguments),
3104 prefix = '';
3105
3106 // Support for the old `pre` argument
3107 if ((typeof args[1] === 'string')) {
3108 prefix = args[1] + '&';
3109 args[1] = false;
3110 }
3111
3112 return prefix + ExtObject.toQueryString.apply(ExtObject, args);
3113 };
3114
3115 /**
3116 * A convenient alias method for {@link Ext.Object#fromQueryString}.
3117 *
3118 * @member Ext
3119 * @method urlDecode
3120 * @deprecated 2.0.0 Please use {@link Ext.Object#fromQueryString Ext.Object.fromQueryString} instead
3121 */
3122 Ext.urlDecode = function() {
3123 return ExtObject.fromQueryString.apply(ExtObject, arguments);
3124 };
3125
3126 })();
3127
3128 //@tag foundation,core
3129 //@define Ext.Function
3130 //@require Ext.Object
3131
3132 /**
3133 * @class Ext.Function
3134 *
3135 * A collection of useful static methods to deal with function callbacks.
3136 * @singleton
3137 * @alternateClassName Ext.util.Functions
3138 */
3139 Ext.Function = {
3140
3141 /**
3142 * A very commonly used method throughout the framework. It acts as a wrapper around another method
3143 * which originally accepts 2 arguments for `name` and `value`.
3144 * The wrapped function then allows "flexible" value setting of either:
3145 *
3146 * - `name` and `value` as 2 arguments
3147 * - one single object argument with multiple key - value pairs
3148 *
3149 * For example:
3150 *
3151 * var setValue = Ext.Function.flexSetter(function(name, value) {
3152 * this[name] = value;
3153 * });
3154 *
3155 * // Afterwards
3156 * // Setting a single name - value
3157 * setValue('name1', 'value1');
3158 *
3159 * // Settings multiple name - value pairs
3160 * setValue({
3161 * name1: 'value1',
3162 * name2: 'value2',
3163 * name3: 'value3'
3164 * });
3165 *
3166 * @param {Function} fn
3167 * @return {Function} flexSetter
3168 */
3169 flexSetter: function(fn) {
3170 return function(a, b) {
3171 var k, i;
3172
3173 if (a === null) {
3174 return this;
3175 }
3176
3177 if (typeof a !== 'string') {
3178 for (k in a) {
3179 if (a.hasOwnProperty(k)) {
3180 fn.call(this, k, a[k]);
3181 }
3182 }
3183
3184 if (Ext.enumerables) {
3185 for (i = Ext.enumerables.length; i--;) {
3186 k = Ext.enumerables[i];
3187 if (a.hasOwnProperty(k)) {
3188 fn.call(this, k, a[k]);
3189 }
3190 }
3191 }
3192 } else {
3193 fn.call(this, a, b);
3194 }
3195
3196 return this;
3197 };
3198 },
3199
3200 /**
3201 * Create a new function from the provided `fn`, change `this` to the provided scope, optionally
3202 * overrides arguments for the call. Defaults to the arguments passed by the caller.
3203 *
3204 * {@link Ext#bind Ext.bind} is alias for {@link Ext.Function#bind Ext.Function.bind}
3205 *
3206 * @param {Function} fn The function to delegate.
3207 * @param {Object} scope (optional) The scope (`this` reference) in which the function is executed.
3208 * **If omitted, defaults to the browser window.**
3209 * @param {Array} args (optional) Overrides arguments for the call. (Defaults to the arguments passed by the caller)
3210 * @param {Boolean/Number} appendArgs (optional) if `true` args are appended to call args instead of overriding,
3211 * if a number the args are inserted at the specified position.
3212 * @return {Function} The new function.
3213 */
3214 bind: function(fn, scope, args, appendArgs) {
3215 if (arguments.length === 2) {
3216 return function() {
3217 return fn.apply(scope, arguments);
3218 }
3219 }
3220
3221 var method = fn,
3222 slice = Array.prototype.slice;
3223
3224 return function() {
3225 var callArgs = args || arguments;
3226
3227 if (appendArgs === true) {
3228 callArgs = slice.call(arguments, 0);
3229 callArgs = callArgs.concat(args);
3230 }
3231 else if (typeof appendArgs == 'number') {
3232 callArgs = slice.call(arguments, 0); // copy arguments first
3233 Ext.Array.insert(callArgs, appendArgs, args);
3234 }
3235
3236 return method.apply(scope || window, callArgs);
3237 };
3238 },
3239
3240 /**
3241 * Create a new function from the provided `fn`, the arguments of which are pre-set to `args`.
3242 * New arguments passed to the newly created callback when it's invoked are appended after the pre-set ones.
3243 * This is especially useful when creating callbacks.
3244 *
3245 * For example:
3246 *
3247 * var originalFunction = function(){
3248 * alert(Ext.Array.from(arguments).join(' '));
3249 * };
3250 *
3251 * var callback = Ext.Function.pass(originalFunction, ['Hello', 'World']);
3252 *
3253 * callback(); // alerts 'Hello World'
3254 * callback('by Me'); // alerts 'Hello World by Me'
3255 *
3256 * {@link Ext#pass Ext.pass} is alias for {@link Ext.Function#pass Ext.Function.pass}
3257 *
3258 * @param {Function} fn The original function.
3259 * @param {Array} args The arguments to pass to new callback.
3260 * @param {Object} scope (optional) The scope (`this` reference) in which the function is executed.
3261 * @return {Function} The new callback function.
3262 */
3263 pass: function(fn, args, scope) {
3264 if (!Ext.isArray(args)) {
3265 args = Ext.Array.clone(args);
3266 }
3267
3268 return function() {
3269 args.push.apply(args, arguments);
3270 return fn.apply(scope || this, args);
3271 };
3272 },
3273
3274 /**
3275 * Create an alias to the provided method property with name `methodName` of `object`.
3276 * Note that the execution scope will still be bound to the provided `object` itself.
3277 *
3278 * @param {Object/Function} object
3279 * @param {String} methodName
3280 * @return {Function} aliasFn
3281 */
3282 alias: function(object, methodName) {
3283 return function() {
3284 return object[methodName].apply(object, arguments);
3285 };
3286 },
3287
3288 /**
3289 * Create a "clone" of the provided method. The returned method will call the given
3290 * method passing along all arguments and the "this" pointer and return its result.
3291 *
3292 * @param {Function} method
3293 * @return {Function} cloneFn
3294 */
3295 clone: function(method) {
3296 return function() {
3297 return method.apply(this, arguments);
3298 };
3299 },
3300
3301 /**
3302 * Creates an interceptor function. The passed function is called before the original one. If it returns false,
3303 * the original one is not called. The resulting function returns the results of the original function.
3304 * The passed function is called with the parameters of the original function. Example usage:
3305 *
3306 * var sayHi = function(name){
3307 * alert('Hi, ' + name);
3308 * };
3309 *
3310 * sayHi('Fred'); // alerts "Hi, Fred"
3311 *
3312 * // create a new function that validates input without
3313 * // directly modifying the original function:
3314 * var sayHiToFriend = Ext.Function.createInterceptor(sayHi, function(name){
3315 * return name === 'Brian';
3316 * });
3317 *
3318 * sayHiToFriend('Fred'); // no alert
3319 * sayHiToFriend('Brian'); // alerts "Hi, Brian"
3320 *
3321 * @param {Function} origFn The original function.
3322 * @param {Function} newFn The function to call before the original.
3323 * @param {Object} scope (optional) The scope (`this` reference) in which the passed function is executed.
3324 * **If omitted, defaults to the scope in which the original function is called or the browser window.**
3325 * @param {Object} [returnValue=null] (optional) The value to return if the passed function return `false`.
3326 * @return {Function} The new function.
3327 */
3328 createInterceptor: function(origFn, newFn, scope, returnValue) {
3329 var method = origFn;
3330 if (!Ext.isFunction(newFn)) {
3331 return origFn;
3332 }
3333 else {
3334 return function() {
3335 var me = this,
3336 args = arguments;
3337 newFn.target = me;
3338 newFn.method = origFn;
3339 return (newFn.apply(scope || me || window, args) !== false) ? origFn.apply(me || window, args) : returnValue || null;
3340 };
3341 }
3342 },
3343
3344 /**
3345 * Creates a delegate (callback) which, when called, executes after a specific delay.
3346 *
3347 * @param {Function} fn The function which will be called on a delay when the returned function is called.
3348 * Optionally, a replacement (or additional) argument list may be specified.
3349 * @param {Number} delay The number of milliseconds to defer execution by whenever called.
3350 * @param {Object} scope (optional) The scope (`this` reference) used by the function at execution time.
3351 * @param {Array} args (optional) Override arguments for the call. (Defaults to the arguments passed by the caller)
3352 * @param {Boolean/Number} appendArgs (optional) if True args are appended to call args instead of overriding,
3353 * if a number the args are inserted at the specified position.
3354 * @return {Function} A function which, when called, executes the original function after the specified delay.
3355 */
3356 createDelayed: function(fn, delay, scope, args, appendArgs) {
3357 if (scope || args) {
3358 fn = Ext.Function.bind(fn, scope, args, appendArgs);
3359 }
3360
3361 return function() {
3362 var me = this,
3363 args = Array.prototype.slice.call(arguments);
3364
3365 setTimeout(function() {
3366 fn.apply(me, args);
3367 }, delay);
3368 }
3369 },
3370
3371 /**
3372 * Calls this function after the number of milliseconds specified, optionally in a specific scope. Example usage:
3373 *
3374 * var sayHi = function(name){
3375 * alert('Hi, ' + name);
3376 * };
3377 *
3378 * // executes immediately:
3379 * sayHi('Fred');
3380 *
3381 * // executes after 2 seconds:
3382 * Ext.Function.defer(sayHi, 2000, this, ['Fred']);
3383 *
3384 * // this syntax is sometimes useful for deferring
3385 * // execution of an anonymous function:
3386 * Ext.Function.defer(function(){
3387 * alert('Anonymous');
3388 * }, 100);
3389 *
3390 * {@link Ext#defer Ext.defer} is alias for {@link Ext.Function#defer Ext.Function.defer}
3391 *
3392 * @param {Function} fn The function to defer.
3393 * @param {Number} millis The number of milliseconds for the `setTimeout()` call.
3394 * If less than or equal to 0 the function is executed immediately.
3395 * @param {Object} scope (optional) The scope (`this` reference) in which the function is executed.
3396 * If omitted, defaults to the browser window.
3397 * @param {Array} args (optional) Overrides arguments for the call. Defaults to the arguments passed by the caller.
3398 * @param {Boolean/Number} appendArgs (optional) if `true`, args are appended to call args instead of overriding,
3399 * if a number the args are inserted at the specified position.
3400 * @return {Number} The timeout id that can be used with `clearTimeout()`.
3401 */
3402 defer: function(fn, millis, scope, args, appendArgs) {
3403 fn = Ext.Function.bind(fn, scope, args, appendArgs);
3404 if (millis > 0) {
3405 return setTimeout(fn, millis);
3406 }
3407 fn();
3408 return 0;
3409 },
3410
3411 /**
3412 * Create a combined function call sequence of the original function + the passed function.
3413 * The resulting function returns the results of the original function.
3414 * The passed function is called with the parameters of the original function. Example usage:
3415 *
3416 * var sayHi = function(name){
3417 * alert('Hi, ' + name);
3418 * };
3419 *
3420 * sayHi('Fred'); // alerts "Hi, Fred"
3421 *
3422 * var sayGoodbye = Ext.Function.createSequence(sayHi, function(name){
3423 * alert('Bye, ' + name);
3424 * });
3425 *
3426 * sayGoodbye('Fred'); // both alerts show
3427 *
3428 * @param {Function} originalFn The original function.
3429 * @param {Function} newFn The function to sequence.
3430 * @param {Object} scope (optional) The scope (`this` reference) in which the passed function is executed.
3431 * If omitted, defaults to the scope in which the original function is called or the browser window.
3432 * @return {Function} The new function.
3433 */
3434 createSequence: function(originalFn, newFn, scope) {
3435 if (!newFn) {
3436 return originalFn;
3437 }
3438 else {
3439 return function() {
3440 var result = originalFn.apply(this, arguments);
3441 newFn.apply(scope || this, arguments);
3442 return result;
3443 };
3444 }
3445 },
3446
3447 /**
3448 * Creates a delegate function, optionally with a bound scope which, when called, buffers
3449 * the execution of the passed function for the configured number of milliseconds.
3450 * If called again within that period, the impending invocation will be canceled, and the
3451 * timeout period will begin again.
3452 *
3453 * @param {Function} fn The function to invoke on a buffered timer.
3454 * @param {Number} buffer The number of milliseconds by which to buffer the invocation of the
3455 * function.
3456 * @param {Object} scope (optional) The scope (`this` reference) in which
3457 * the passed function is executed. If omitted, defaults to the scope specified by the caller.
3458 * @param {Array} args (optional) Override arguments for the call. Defaults to the arguments
3459 * passed by the caller.
3460 * @return {Function} A function which invokes the passed function after buffering for the specified time.
3461 */
3462
3463 createBuffered: function(fn, buffer, scope, args) {
3464 var timerId;
3465
3466 return function() {
3467 var callArgs = args || Array.prototype.slice.call(arguments, 0),
3468 me = scope || this;
3469
3470 if (timerId) {
3471 clearTimeout(timerId);
3472 }
3473
3474 timerId = setTimeout(function(){
3475 fn.apply(me, callArgs);
3476 }, buffer);
3477 };
3478 },
3479
3480 /**
3481 * Creates a throttled version of the passed function which, when called repeatedly and
3482 * rapidly, invokes the passed function only after a certain interval has elapsed since the
3483 * previous invocation.
3484 *
3485 * This is useful for wrapping functions which may be called repeatedly, such as
3486 * a handler of a mouse move event when the processing is expensive.
3487 *
3488 * @param {Function} fn The function to execute at a regular time interval.
3489 * @param {Number} interval The interval, in milliseconds, on which the passed function is executed.
3490 * @param {Object} scope (optional) The scope (`this` reference) in which
3491 * the passed function is executed. If omitted, defaults to the scope specified by the caller.
3492 * @return {Function} A function which invokes the passed function at the specified interval.
3493 */
3494 createThrottled: function(fn, interval, scope) {
3495 var lastCallTime, elapsed, lastArgs, timer, execute = function() {
3496 fn.apply(scope || this, lastArgs);
3497 lastCallTime = new Date().getTime();
3498 };
3499
3500 return function() {
3501 elapsed = new Date().getTime() - lastCallTime;
3502 lastArgs = arguments;
3503
3504 clearTimeout(timer);
3505 if (!lastCallTime || (elapsed >= interval)) {
3506 execute();
3507 } else {
3508 timer = setTimeout(execute, interval - elapsed);
3509 }
3510 };
3511 },
3512
3513
3514 /**
3515 * Adds behavior to an existing method that is executed before the
3516 * original behavior of the function. For example:
3517 *
3518 * var soup = {
3519 * contents: [],
3520 * add: function(ingredient) {
3521 * this.contents.push(ingredient);
3522 * }
3523 * };
3524 * Ext.Function.interceptBefore(soup, "add", function(ingredient){
3525 * if (!this.contents.length && ingredient !== "water") {
3526 * // Always add water to start with
3527 * this.contents.push("water");
3528 * }
3529 * });
3530 * soup.add("onions");
3531 * soup.add("salt");
3532 * soup.contents; // will contain: water, onions, salt
3533 *
3534 * @param {Object} object The target object
3535 * @param {String} methodName Name of the method to override
3536 * @param {Function} fn Function with the new behavior. It will
3537 * be called with the same arguments as the original method. The
3538 * return value of this function will be the return value of the
3539 * new method.
3540 * @param {Object} [scope] The scope to execute the interceptor function. Defaults to the object.
3541 * @return {Function} The new function just created.
3542 */
3543 interceptBefore: function(object, methodName, fn, scope) {
3544 var method = object[methodName] || Ext.emptyFn;
3545
3546 return (object[methodName] = function() {
3547 var ret = fn.apply(scope || this, arguments);
3548 method.apply(this, arguments);
3549
3550 return ret;
3551 });
3552 },
3553
3554 /**
3555 * Adds behavior to an existing method that is executed after the
3556 * original behavior of the function. For example:
3557 *
3558 * var soup = {
3559 * contents: [],
3560 * add: function(ingredient) {
3561 * this.contents.push(ingredient);
3562 * }
3563 * };
3564 * Ext.Function.interceptAfter(soup, "add", function(ingredient){
3565 * // Always add a bit of extra salt
3566 * this.contents.push("salt");
3567 * });
3568 * soup.add("water");
3569 * soup.add("onions");
3570 * soup.contents; // will contain: water, salt, onions, salt
3571 *
3572 * @param {Object} object The target object
3573 * @param {String} methodName Name of the method to override
3574 * @param {Function} fn Function with the new behavior. It will
3575 * be called with the same arguments as the original method. The
3576 * return value of this function will be the return value of the
3577 * new method.
3578 * @param {Object} [scope] The scope to execute the interceptor function. Defaults to the object.
3579 * @return {Function} The new function just created.
3580 */
3581 interceptAfter: function(object, methodName, fn, scope) {
3582 var method = object[methodName] || Ext.emptyFn;
3583
3584 return (object[methodName] = function() {
3585 method.apply(this, arguments);
3586 return fn.apply(scope || this, arguments);
3587 });
3588 }
3589 };
3590
3591 /**
3592 * @method
3593 * @member Ext
3594 * @alias Ext.Function#defer
3595 */
3596 Ext.defer = Ext.Function.alias(Ext.Function, 'defer');
3597
3598 /**
3599 * @method
3600 * @member Ext
3601 * @alias Ext.Function#pass
3602 */
3603 Ext.pass = Ext.Function.alias(Ext.Function, 'pass');
3604
3605 /**
3606 * @method
3607 * @member Ext
3608 * @alias Ext.Function#bind
3609 */
3610 Ext.bind = Ext.Function.alias(Ext.Function, 'bind');
3611
3612 //@tag foundation,core
3613 //@define Ext.JSON
3614 //@require Ext.Function
3615
3616 /**
3617 * @class Ext.JSON
3618 * Modified version of Douglas Crockford's json.js that doesn't
3619 * mess with the Object prototype.
3620 * [http://www.json.org/js.html](http://www.json.org/js.html)
3621 * @singleton
3622 */
3623 Ext.JSON = new(function() {
3624 var useHasOwn = !! {}.hasOwnProperty,
3625 isNative = function() {
3626 var useNative = null;
3627
3628 return function() {
3629 if (useNative === null) {
3630 useNative = Ext.USE_NATIVE_JSON && window.JSON && JSON.toString() == '[object JSON]';
3631 }
3632
3633 return useNative;
3634 };
3635 }(),
3636 pad = function(n) {
3637 return n < 10 ? "0" + n : n;
3638 },
3639 doDecode = function(json) {
3640 return eval("(" + json + ')');
3641 },
3642 doEncode = function(o) {
3643 if (!Ext.isDefined(o) || o === null) {
3644 return "null";
3645 } else if (Ext.isArray(o)) {
3646 return encodeArray(o);
3647 } else if (Ext.isDate(o)) {
3648 return Ext.JSON.encodeDate(o);
3649 } else if (Ext.isString(o)) {
3650 if (Ext.isMSDate(o)) {
3651 return encodeMSDate(o);
3652 } else {
3653 return encodeString(o);
3654 }
3655 } else if (typeof o == "number") {
3656 //don't use isNumber here, since finite checks happen inside isNumber
3657 return isFinite(o) ? String(o) : "null";
3658 } else if (Ext.isBoolean(o)) {
3659 return String(o);
3660 } else if (Ext.isObject(o)) {
3661 return encodeObject(o);
3662 } else if (typeof o === "function") {
3663 return "null";
3664 }
3665 return 'undefined';
3666 },
3667 m = {
3668 "\b": '\\b',
3669 "\t": '\\t',
3670 "\n": '\\n',
3671 "\f": '\\f',
3672 "\r": '\\r',
3673 '"': '\\"',
3674 "\\": '\\\\',
3675 '\x0b': '\\u000b' //ie doesn't handle \v
3676 },
3677 charToReplace = /[\\\"\x00-\x1f\x7f-\uffff]/g,
3678 encodeString = function(s) {
3679 return '"' + s.replace(charToReplace, function(a) {
3680 var c = m[a];
3681 return typeof c === 'string' ? c : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
3682 }) + '"';
3683 },
3684 encodeArray = function(o) {
3685 var a = ["[", ""],
3686 // Note empty string in case there are no serializable members.
3687 len = o.length,
3688 i;
3689 for (i = 0; i < len; i += 1) {
3690 a.push(doEncode(o[i]), ',');
3691 }
3692 // Overwrite trailing comma (or empty string)
3693 a[a.length - 1] = ']';
3694 return a.join("");
3695 },
3696 encodeObject = function(o) {
3697 var a = ["{", ""],
3698 // Note empty string in case there are no serializable members.
3699 i;
3700 for (i in o) {
3701 if (!useHasOwn || o.hasOwnProperty(i)) {
3702 a.push(doEncode(i), ":", doEncode(o[i]), ',');
3703 }
3704 }
3705 // Overwrite trailing comma (or empty string)
3706 a[a.length - 1] = '}';
3707 return a.join("");
3708 },
3709 encodeMSDate = function(o) {
3710 return '"' + o + '"';
3711 };
3712
3713 /**
3714 * Encodes a Date. This returns the actual string which is inserted into the JSON string as the literal expression.
3715 * __The returned value includes enclosing double quotation marks.__
3716 *
3717 * The default return format is "yyyy-mm-ddThh:mm:ss".
3718 *
3719 * To override this:
3720 *
3721 * Ext.JSON.encodeDate = function(d) {
3722 * return Ext.Date.format(d, '"Y-m-d"');
3723 * };
3724 *
3725 * @param {Date} d The Date to encode.
3726 * @return {String} The string literal to use in a JSON string.
3727 */
3728 this.encodeDate = function(o) {
3729 return '"' + o.getFullYear() + "-"
3730 + pad(o.getMonth() + 1) + "-"
3731 + pad(o.getDate()) + "T"
3732 + pad(o.getHours()) + ":"
3733 + pad(o.getMinutes()) + ":"
3734 + pad(o.getSeconds()) + '"';
3735 };
3736
3737 /**
3738 * Encodes an Object, Array or other value.
3739 * @param {Object} o The variable to encode.
3740 * @return {String} The JSON string.
3741 * @method
3742 */
3743 this.encode = function() {
3744 var ec;
3745 return function(o) {
3746 if (!ec) {
3747 // setup encoding function on first access
3748 ec = isNative() ? JSON.stringify : doEncode;
3749 }
3750 return ec(o);
3751 };
3752 }();
3753
3754
3755 /**
3756 * Decodes (parses) a JSON string to an object. If the JSON is invalid, this function throws a Error unless the safe option is set.
3757 * @param {String} json The JSON string.
3758 * @param {Boolean} safe (optional) Whether to return `null` or throw an exception if the JSON is invalid.
3759 * @return {Object/null} The resulting object.
3760 * @method
3761 */
3762 this.decode = function() {
3763 var dc;
3764 return function(json, safe) {
3765 if (!dc) {
3766 // setup decoding function on first access
3767 dc = isNative() ? JSON.parse : doDecode;
3768 }
3769 try {
3770 return dc(json);
3771 } catch (e) {
3772 if (safe === true) {
3773 return null;
3774 }
3775 Ext.Error.raise({
3776 sourceClass: "Ext.JSON",
3777 sourceMethod: "decode",
3778 msg: "You're trying to decode an invalid JSON String: " + json
3779 });
3780 }
3781 };
3782 }();
3783
3784 })();
3785
3786 //@private Alias for backwards compatibility
3787 if (!Ext.util) {
3788 Ext.util = {};
3789 }
3790 Ext.util.JSON = Ext.JSON;
3791
3792 /**
3793 * Shorthand for {@link Ext.JSON#encode}.
3794 * @member Ext
3795 * @method encode
3796 * @alias Ext.JSON#encode
3797 */
3798 Ext.encode = Ext.JSON.encode;
3799 /**
3800 * Shorthand for {@link Ext.JSON#decode}.
3801 * @member Ext
3802 * @method decode
3803 * @alias Ext.JSON#decode
3804 */
3805 Ext.decode = Ext.JSON.decode;
3806
3807
3808 //@tag foundation,core
3809 //@define Ext.Error
3810 //@require Ext.JSON
3811
3812 Ext.Error = {
3813 raise: function(object) {
3814 throw new Error(object.msg);
3815 }
3816 };
3817
3818 //@tag foundation,core
3819 //@define Ext.Date
3820 //@require Ext.Error
3821
3822 /**
3823 * Note: Date values are zero-based.
3824 */
3825 Ext.Date = {
3826 /** @ignore */
3827 now: Date.now,
3828
3829 /**
3830 * @private
3831 * Private for now
3832 */
3833 toString: function(date) {
3834 if (!date) {
3835 date = new Date();
3836 }
3837
3838 var pad = Ext.String.leftPad;
3839
3840 return date.getFullYear() + "-"
3841 + pad(date.getMonth() + 1, 2, '0') + "-"
3842 + pad(date.getDate(), 2, '0') + "T"
3843 + pad(date.getHours(), 2, '0') + ":"
3844 + pad(date.getMinutes(), 2, '0') + ":"
3845 + pad(date.getSeconds(), 2, '0');
3846 }
3847 };
3848
3849
3850 //@tag foundation,core
3851 //@define Ext.Base
3852 //@require Ext.Date
3853
3854 /**
3855 * @class Ext.Base
3856 * @author Jacky Nguyen <jacky@sencha.com>
3857 *
3858 * The root of all classes created with {@link Ext#define}.
3859 *
3860 * Ext.Base is the building block of all Ext classes. All classes in Ext inherit from Ext.Base. All prototype and static
3861 * members of this class are inherited by all other classes.
3862 *
3863 * See the [Class System Guide](../../../core_concepts/class_system.html) for more.
3864 *
3865 */
3866 (function(flexSetter) {
3867
3868 var noArgs = [],
3869 Base = function(){};
3870
3871 // These static properties will be copied to every newly created class with {@link Ext#define}
3872 Ext.apply(Base, {
3873 $className: 'Ext.Base',
3874
3875 $isClass: true,
3876
3877 /**
3878 * Create a new instance of this Class.
3879 *
3880 * Ext.define('My.cool.Class', {
3881 * // ...
3882 * });
3883 *
3884 * My.cool.Class.create({
3885 * someConfig: true
3886 * });
3887 *
3888 * All parameters are passed to the constructor of the class.
3889 *
3890 * @return {Object} the created instance.
3891 * @static
3892 * @inheritable
3893 */
3894 create: function() {
3895 return Ext.create.apply(Ext, [this].concat(Array.prototype.slice.call(arguments, 0)));
3896 },
3897
3898 /**
3899 * @private
3900 * @static
3901 * @inheritable
3902 */
3903 extend: function(parent) {
3904 var parentPrototype = parent.prototype,
3905 prototype, i, ln, name, statics;
3906
3907 prototype = this.prototype = Ext.Object.chain(parentPrototype);
3908 prototype.self = this;
3909
3910 this.superclass = prototype.superclass = parentPrototype;
3911
3912 if (!parent.$isClass) {
3913 Ext.apply(prototype, Ext.Base.prototype);
3914 prototype.constructor = function() {
3915 parentPrototype.constructor.apply(this, arguments);
3916 };
3917 }
3918
3919 //<feature classSystem.inheritableStatics>
3920 // Statics inheritance
3921 statics = parentPrototype.$inheritableStatics;
3922
3923 if (statics) {
3924 for (i = 0,ln = statics.length; i < ln; i++) {
3925 name = statics[i];
3926
3927 if (!this.hasOwnProperty(name)) {
3928 this[name] = parent[name];
3929 }
3930 }
3931 }
3932 //</feature>
3933
3934 if (parent.$onExtended) {
3935 this.$onExtended = parent.$onExtended.slice();
3936 }
3937
3938 //<feature classSystem.config>
3939 prototype.config = prototype.defaultConfig = new prototype.configClass;
3940 prototype.initConfigList = prototype.initConfigList.slice();
3941 prototype.initConfigMap = Ext.Object.chain(prototype.initConfigMap);
3942 //</feature>
3943 },
3944
3945 /**
3946 * @private
3947 * @static
3948 * @inheritable
3949 */
3950 '$onExtended': [],
3951
3952 /**
3953 * @private
3954 * @static
3955 * @inheritable
3956 */
3957 triggerExtended: function() {
3958 var callbacks = this.$onExtended,
3959 ln = callbacks.length,
3960 i, callback;
3961
3962 if (ln > 0) {
3963 for (i = 0; i < ln; i++) {
3964 callback = callbacks[i];
3965 callback.fn.apply(callback.scope || this, arguments);
3966 }
3967 }
3968 },
3969
3970 /**
3971 * @private
3972 * @static
3973 * @inheritable
3974 */
3975 onExtended: function(fn, scope) {
3976 this.$onExtended.push({
3977 fn: fn,
3978 scope: scope
3979 });
3980
3981 return this;
3982 },
3983
3984 /**
3985 * @private
3986 * @static
3987 * @inheritable
3988 */
3989 addConfig: function(config, fullMerge) {
3990 var prototype = this.prototype,
3991 initConfigList = prototype.initConfigList,
3992 initConfigMap = prototype.initConfigMap,
3993 defaultConfig = prototype.defaultConfig,
3994 hasInitConfigItem, name, value;
3995
3996 fullMerge = Boolean(fullMerge);
3997
3998 for (name in config) {
3999 if (config.hasOwnProperty(name) && (fullMerge || !(name in defaultConfig))) {
4000 value = config[name];
4001 hasInitConfigItem = initConfigMap[name];
4002
4003 if (value !== null) {
4004 if (!hasInitConfigItem) {
4005 initConfigMap[name] = true;
4006 initConfigList.push(name);
4007 }
4008 }
4009 else if (hasInitConfigItem) {
4010 initConfigMap[name] = false;
4011 Ext.Array.remove(initConfigList, name);
4012 }
4013 }
4014 }
4015
4016 if (fullMerge) {
4017 Ext.merge(defaultConfig, config);
4018 }
4019 else {
4020 Ext.mergeIf(defaultConfig, config);
4021 }
4022
4023 prototype.configClass = Ext.Object.classify(defaultConfig);
4024 },
4025
4026 /**
4027 * Add / override static properties of this class.
4028 *
4029 * Ext.define('My.cool.Class', {
4030 * // this.se
4031 * });
4032 *
4033 * My.cool.Class.addStatics({
4034 * someProperty: 'someValue', // My.cool.Class.someProperty = 'someValue'
4035 * method1: function() { }, // My.cool.Class.method1 = function() { ... };
4036 * method2: function() { } // My.cool.Class.method2 = function() { ... };
4037 * });
4038 *
4039 * @param {Object} members
4040 * @return {Ext.Base} this
4041 * @static
4042 * @inheritable
4043 */
4044 addStatics: function(members) {
4045 var member, name;
4046 //<debug>
4047 var className = Ext.getClassName(this);
4048 //</debug>
4049
4050 for (name in members) {
4051 if (members.hasOwnProperty(name)) {
4052 member = members[name];
4053 //<debug>
4054 if (typeof member == 'function') {
4055 member.displayName = className + '.' + name;
4056 }
4057 //</debug>
4058 this[name] = member;
4059 }
4060 }
4061
4062 return this;
4063 },
4064
4065 /**
4066 * @private
4067 * @static
4068 * @inheritable
4069 */
4070 addInheritableStatics: function(members) {
4071 var inheritableStatics,
4072 hasInheritableStatics,
4073 prototype = this.prototype,
4074 name, member;
4075
4076 inheritableStatics = prototype.$inheritableStatics;
4077 hasInheritableStatics = prototype.$hasInheritableStatics;
4078
4079 if (!inheritableStatics) {
4080 inheritableStatics = prototype.$inheritableStatics = [];
4081 hasInheritableStatics = prototype.$hasInheritableStatics = {};
4082 }
4083
4084 //<debug>
4085 var className = Ext.getClassName(this);
4086 //</debug>
4087
4088 for (name in members) {
4089 if (members.hasOwnProperty(name)) {
4090 member = members[name];
4091 //<debug>
4092 if (typeof member == 'function') {
4093 member.displayName = className + '.' + name;
4094 }
4095 //</debug>
4096 this[name] = member;
4097
4098 if (!hasInheritableStatics[name]) {
4099 hasInheritableStatics[name] = true;
4100 inheritableStatics.push(name);
4101 }
4102 }
4103 }
4104
4105 return this;
4106 },
4107
4108 /**
4109 * Add methods / properties to the prototype of this class.
4110 *
4111 * @example
4112 * Ext.define('My.awesome.Cat', {
4113 * constructor: function() {
4114 * // ...
4115 * }
4116 * });
4117 *
4118 * My.awesome.Cat.addMembers({
4119 * meow: function() {
4120 * alert('Meowww...');
4121 * }
4122 * });
4123 *
4124 * var kitty = new My.awesome.Cat();
4125 * kitty.meow();
4126 *
4127 * @param {Object} members
4128 * @static
4129 * @inheritable
4130 */
4131 addMembers: function(members) {
4132 var prototype = this.prototype,
4133 names = [],
4134 name, member;
4135
4136 //<debug>
4137 var className = this.$className || '';
4138 //</debug>
4139
4140 for (name in members) {
4141 if (members.hasOwnProperty(name)) {
4142 member = members[name];
4143
4144 if (typeof member == 'function' && !member.$isClass && member !== Ext.emptyFn) {
4145 member.$owner = this;
4146 member.$name = name;
4147 //<debug>
4148 member.displayName = className + '#' + name;
4149 //</debug>
4150 }
4151
4152 prototype[name] = member;
4153 }
4154 }
4155
4156 return this;
4157 },
4158
4159 /**
4160 * @private
4161 * @static
4162 * @inheritable
4163 */
4164 addMember: function(name, member) {
4165 if (typeof member == 'function' && !member.$isClass && member !== Ext.emptyFn) {
4166 member.$owner = this;
4167 member.$name = name;
4168 //<debug>
4169 member.displayName = (this.$className || '') + '#' + name;
4170 //</debug>
4171 }
4172
4173 this.prototype[name] = member;
4174
4175 return this;
4176 },
4177
4178 /**
4179 * @private
4180 * @static
4181 * @inheritable
4182 */
4183 implement: function() {
4184 this.addMembers.apply(this, arguments);
4185 },
4186
4187 /**
4188 * Borrow another class' members to the prototype of this class.
4189 *
4190 * Ext.define('Bank', {
4191 * money: '$$$',
4192 * printMoney: function() {
4193 * alert('$$$$$$$');
4194 * }
4195 * });
4196 *
4197 * Ext.define('Thief', {
4198 * // ...
4199 * });
4200 *
4201 * Thief.borrow(Bank, ['money', 'printMoney']);
4202 *
4203 * var steve = new Thief();
4204 *
4205 * alert(steve.money); // alerts '$$$'
4206 * steve.printMoney(); // alerts '$$$$$$$'
4207 *
4208 * @param {Ext.Base} fromClass The class to borrow members from
4209 * @param {Array/String} members The names of the members to borrow
4210 * @return {Ext.Base} this
4211 * @static
4212 * @inheritable
4213 * @private
4214 */
4215 borrow: function(fromClass, members) {
4216 var prototype = this.prototype,
4217 fromPrototype = fromClass.prototype,
4218 //<debug>
4219 className = Ext.getClassName(this),
4220 //</debug>
4221 i, ln, name, fn, toBorrow;
4222
4223 members = Ext.Array.from(members);
4224
4225 for (i = 0,ln = members.length; i < ln; i++) {
4226 name = members[i];
4227
4228 toBorrow = fromPrototype[name];
4229
4230 if (typeof toBorrow == 'function') {
4231 fn = function() {
4232 return toBorrow.apply(this, arguments);
4233 };
4234
4235 //<debug>
4236 if (className) {
4237 fn.displayName = className + '#' + name;
4238 }
4239 //</debug>
4240
4241 fn.$owner = this;
4242 fn.$name = name;
4243
4244 prototype[name] = fn;
4245 }
4246 else {
4247 prototype[name] = toBorrow;
4248 }
4249 }
4250
4251 return this;
4252 },
4253
4254 /**
4255 * Override members of this class. Overridden methods can be invoked via
4256 * {@link Ext.Base#callParent}.
4257 *
4258 * Ext.define('My.Cat', {
4259 * constructor: function() {
4260 * alert("I'm a cat!");
4261 * }
4262 * });
4263 *
4264 * My.Cat.override({
4265 * constructor: function() {
4266 * alert("I'm going to be a cat!");
4267 *
4268 * var instance = this.callParent(arguments);
4269 *
4270 * alert("Meeeeoooowwww");
4271 *
4272 * return instance;
4273 * }
4274 * });
4275 *
4276 * var kitty = new My.Cat(); // alerts "I'm going to be a cat!"
4277 * // alerts "I'm a cat!"
4278 * // alerts "Meeeeoooowwww"
4279 *
4280 * As of 2.1, direct use of this method is deprecated. Use {@link Ext#define Ext.define}
4281 * instead:
4282 *
4283 * Ext.define('My.CatOverride', {
4284 * override: 'My.Cat',
4285 *
4286 * constructor: function() {
4287 * alert("I'm going to be a cat!");
4288 *
4289 * var instance = this.callParent(arguments);
4290 *
4291 * alert("Meeeeoooowwww");
4292 *
4293 * return instance;
4294 * }
4295 * });
4296 *
4297 * The above accomplishes the same result but can be managed by the {@link Ext.Loader}
4298 * which can properly order the override and its target class and the build process
4299 * can determine whether the override is needed based on the required state of the
4300 * target class (My.Cat).
4301 *
4302 * @param {Object} members The properties to add to this class. This should be
4303 * specified as an object literal containing one or more properties.
4304 * @return {Ext.Base} this class
4305 * @static
4306 * @inheritable
4307 * @deprecated 2.1.0 Please use {@link Ext#define Ext.define} instead
4308 */
4309 override: function(members) {
4310 var me = this,
4311 enumerables = Ext.enumerables,
4312 target = me.prototype,
4313 cloneFunction = Ext.Function.clone,
4314 currentConfig = target.config,
4315 name, index, member, statics, names, previous, newConfig, prop;
4316
4317 if (arguments.length === 2) {
4318 name = members;
4319 members = {};
4320 members[name] = arguments[1];
4321 enumerables = null;
4322 }
4323
4324 do {
4325 names = []; // clean slate for prototype (1st pass) and static (2nd pass)
4326 statics = null; // not needed 1st pass, but needs to be cleared for 2nd pass
4327
4328 for (name in members) { // hasOwnProperty is checked in the next loop...
4329 if (name == 'statics') {
4330 statics = members[name];
4331 }
4332 else if (name == 'config') {
4333 newConfig = members[name];
4334 //<debug error>
4335 for (prop in newConfig) {
4336 if (!(prop in currentConfig)) {
4337 throw new Error("Attempting to override a non-existant config property. This is not " +
4338 "supported, you must extend the Class.");
4339 }
4340 }
4341 //</debug>
4342 me.addConfig(newConfig, true);
4343 }
4344 else {
4345 names.push(name);
4346 }
4347 }
4348
4349 if (enumerables) {
4350 names.push.apply(names, enumerables);
4351 }
4352
4353 for (index = names.length; index--; ) {
4354 name = names[index];
4355
4356 if (members.hasOwnProperty(name)) {
4357 member = members[name];
4358
4359 if (typeof member == 'function' && !member.$className && member !== Ext.emptyFn) {
4360 if (typeof member.$owner != 'undefined') {
4361 member = cloneFunction(member);
4362 }
4363
4364 //<debug>
4365 var className = me.$className;
4366 if (className) {
4367 member.displayName = className + '#' + name;
4368 }
4369 //</debug>
4370
4371 member.$owner = me;
4372 member.$name = name;
4373
4374 previous = target[name];
4375 if (previous) {
4376 member.$previous = previous;
4377 }
4378 }
4379
4380 target[name] = member;
4381 }
4382 }
4383
4384 target = me; // 2nd pass is for statics
4385 members = statics; // statics will be null on 2nd pass
4386 } while (members);
4387
4388 return this;
4389 },
4390
4391 /**
4392 * @protected
4393 * @static
4394 * @inheritable
4395 */
4396 callParent: function(args) {
4397 var method;
4398
4399 // This code is intentionally inlined for the least amount of debugger stepping
4400 return (method = this.callParent.caller) && (method.$previous ||
4401 ((method = method.$owner ? method : method.caller) &&
4402 method.$owner.superclass.$class[method.$name])).apply(this, args || noArgs);
4403 },
4404
4405 //<feature classSystem.mixins>
4406 /**
4407 * Used internally by the mixins pre-processor
4408 * @private
4409 * @static
4410 * @inheritable
4411 */
4412 mixin: function(name, mixinClass) {
4413 var mixin = mixinClass.prototype,
4414 prototype = this.prototype,
4415 key;
4416
4417 if (typeof mixin.onClassMixedIn != 'undefined') {
4418 mixin.onClassMixedIn.call(mixinClass, this);
4419 }
4420
4421 if (!prototype.hasOwnProperty('mixins')) {
4422 if ('mixins' in prototype) {
4423 prototype.mixins = Ext.Object.chain(prototype.mixins);
4424 }
4425 else {
4426 prototype.mixins = {};
4427 }
4428 }
4429
4430 for (key in mixin) {
4431 if (key === 'mixins') {
4432 Ext.merge(prototype.mixins, mixin[key]);
4433 }
4434 else if (typeof prototype[key] == 'undefined' && key != 'mixinId' && key != 'config') {
4435 prototype[key] = mixin[key];
4436 }
4437 }
4438
4439 //<feature classSystem.config>
4440 if ('config' in mixin) {
4441 this.addConfig(mixin.config, false);
4442 }
4443 //</feature>
4444
4445 prototype.mixins[name] = mixin;
4446 },
4447 //</feature>
4448
4449 /**
4450 * Get the current class' name in string format.
4451 *
4452 * Ext.define('My.cool.Class', {
4453 * constructor: function() {
4454 * alert(this.self.getName()); // alerts 'My.cool.Class'
4455 * }
4456 * });
4457 *
4458 * My.cool.Class.getName(); // 'My.cool.Class'
4459 *
4460 * @return {String} className
4461 * @static
4462 * @inheritable
4463 */
4464 getName: function() {
4465 return Ext.getClassName(this);
4466 },
4467
4468 /**
4469 * Create aliases for existing prototype methods. Example:
4470 *
4471 * Ext.define('My.cool.Class', {
4472 * method1: function() { },
4473 * method2: function() { }
4474 * });
4475 *
4476 * var test = new My.cool.Class();
4477 *
4478 * My.cool.Class.createAlias({
4479 * method3: 'method1',
4480 * method4: 'method2'
4481 * });
4482 *
4483 * test.method3(); // test.method1()
4484 *
4485 * My.cool.Class.createAlias('method5', 'method3');
4486 *
4487 * test.method5(); // test.method3() -> test.method1()
4488 *
4489 * @param {String/Object} alias The new method name, or an object to set multiple aliases. See
4490 * {@link Ext.Function#flexSetter flexSetter}
4491 * @param {String/Object} origin The original method name
4492 * @static
4493 * @inheritable
4494 * @method
4495 */
4496 createAlias: flexSetter(function(alias, origin) {
4497 this.override(alias, function() {
4498 return this[origin].apply(this, arguments);
4499 });
4500 }),
4501
4502 /**
4503 * @private
4504 * @static
4505 * @inheritable
4506 */
4507 addXtype: function(xtype) {
4508 var prototype = this.prototype,
4509 xtypesMap = prototype.xtypesMap,
4510 xtypes = prototype.xtypes,
4511 xtypesChain = prototype.xtypesChain;
4512
4513 if (!prototype.hasOwnProperty('xtypesMap')) {
4514 xtypesMap = prototype.xtypesMap = Ext.merge({}, prototype.xtypesMap || {});
4515 xtypes = prototype.xtypes = prototype.xtypes ? [].concat(prototype.xtypes) : [];
4516 xtypesChain = prototype.xtypesChain = prototype.xtypesChain ? [].concat(prototype.xtypesChain) : [];
4517 prototype.xtype = xtype;
4518 }
4519
4520 if (!xtypesMap[xtype]) {
4521 xtypesMap[xtype] = true;
4522 xtypes.push(xtype);
4523 xtypesChain.push(xtype);
4524 Ext.ClassManager.setAlias(this, 'widget.' + xtype);
4525 }
4526
4527 return this;
4528 }
4529 });
4530
4531 Base.implement({
4532 isInstance: true,
4533
4534 $className: 'Ext.Base',
4535
4536 configClass: Ext.emptyFn,
4537
4538 initConfigList: [],
4539
4540 initConfigMap: {},
4541
4542 /**
4543 * Get the reference to the class from which this object was instantiated. Note that unlike {@link Ext.Base#self},
4544 * `this.statics()` is scope-independent and it always returns the class from which it was called, regardless of what
4545 * `this` points to during run-time
4546 *
4547 * Ext.define('My.Cat', {
4548 * statics: {
4549 * totalCreated: 0,
4550 * speciesName: 'Cat' // My.Cat.speciesName = 'Cat'
4551 * },
4552 *
4553 * constructor: function() {
4554 * var statics = this.statics();
4555 *
4556 * alert(statics.speciesName); // always equals to 'Cat' no matter what 'this' refers to
4557 * // equivalent to: My.Cat.speciesName
4558 *
4559 * alert(this.self.speciesName); // dependent on 'this'
4560 *
4561 * statics.totalCreated++;
4562 * },
4563 *
4564 * clone: function() {
4565 * var cloned = new this.self(); // dependent on 'this'
4566 *
4567 * cloned.groupName = this.statics().speciesName; // equivalent to: My.Cat.speciesName
4568 *
4569 * return cloned;
4570 * }
4571 * });
4572 *
4573 *
4574 * Ext.define('My.SnowLeopard', {
4575 * extend: 'My.Cat',
4576 *
4577 * statics: {
4578 * speciesName: 'Snow Leopard' // My.SnowLeopard.speciesName = 'Snow Leopard'
4579 * },
4580 *
4581 * constructor: function() {
4582 * this.callParent();
4583 * }
4584 * });
4585 *
4586 * var cat = new My.Cat(); // alerts 'Cat', then alerts 'Cat'
4587 *
4588 * var snowLeopard = new My.SnowLeopard(); // alerts 'Cat', then alerts 'Snow Leopard'
4589 *
4590 * var clone = snowLeopard.clone();
4591 * alert(Ext.getClassName(clone)); // alerts 'My.SnowLeopard'
4592 * alert(clone.groupName); // alerts 'Cat'
4593 *
4594 * alert(My.Cat.totalCreated); // alerts 3
4595 *
4596 * @protected
4597 * @return {Ext.Class}
4598 */
4599 statics: function() {
4600 var method = this.statics.caller,
4601 self = this.self;
4602
4603 if (!method) {
4604 return self;
4605 }
4606
4607 return method.$owner;
4608 },
4609
4610 /**
4611 * Call the "parent" method of the current method. That is the method previously
4612 * overridden by derivation or by an override (see {@link Ext#define}).
4613 *
4614 * Ext.define('My.Base', {
4615 * constructor: function (x) {
4616 * this.x = x;
4617 * },
4618 *
4619 * statics: {
4620 * method: function (x) {
4621 * return x;
4622 * }
4623 * }
4624 * });
4625 *
4626 * Ext.define('My.Derived', {
4627 * extend: 'My.Base',
4628 *
4629 * constructor: function () {
4630 * this.callParent([21]);
4631 * }
4632 * });
4633 *
4634 * var obj = new My.Derived();
4635 *
4636 * alert(obj.x); // alerts 21
4637 *
4638 * This can be used with an override as follows:
4639 *
4640 * Ext.define('My.DerivedOverride', {
4641 * override: 'My.Derived',
4642 *
4643 * constructor: function (x) {
4644 * this.callParent([x*2]); // calls original My.Derived constructor
4645 * }
4646 * });
4647 *
4648 * var obj = new My.Derived();
4649 *
4650 * alert(obj.x); // now alerts 42
4651 *
4652 * This also works with static methods.
4653 *
4654 * Ext.define('My.Derived2', {
4655 * extend: 'My.Base',
4656 *
4657 * statics: {
4658 * method: function (x) {
4659 * return this.callParent([x*2]); // calls My.Base.method
4660 * }
4661 * }
4662 * });
4663 *
4664 * alert(My.Base.method(10)); // alerts 10
4665 * alert(My.Derived2.method(10)); // alerts 20
4666 *
4667 * Lastly, it also works with overridden static methods.
4668 *
4669 * Ext.define('My.Derived2Override', {
4670 * override: 'My.Derived2',
4671 *
4672 * statics: {
4673 * method: function (x) {
4674 * return this.callParent([x*2]); // calls My.Derived2.method
4675 * }
4676 * }
4677 * });
4678 *
4679 * alert(My.Derived2.method(10)); // now alerts 40
4680 *
4681 * To override a method and replace it and also call the superclass method, use
4682 * {@link #callSuper}. This is often done to patch a method to fix a bug.
4683 *
4684 * @protected
4685 * @param {Array/Arguments} args The arguments, either an array or the `arguments` object
4686 * from the current method, for example: `this.callParent(arguments)`
4687 * @return {Object} Returns the result of calling the parent method
4688 */
4689 callParent: function(args) {
4690 // NOTE: this code is deliberately as few expressions (and no function calls)
4691 // as possible so that a debugger can skip over this noise with the minimum number
4692 // of steps. Basically, just hit Step Into until you are where you really wanted
4693 // to be.
4694 var method,
4695 superMethod = (method = this.callParent.caller) && (method.$previous ||
4696 ((method = method.$owner ? method : method.caller) &&
4697 method.$owner.superclass[method.$name]));
4698
4699 //<debug error>
4700 if (!superMethod) {
4701 method = this.callParent.caller;
4702 var parentClass, methodName;
4703
4704 if (!method.$owner) {
4705 if (!method.caller) {
4706 throw new Error("Attempting to call a protected method from the public scope, which is not allowed");
4707 }
4708
4709 method = method.caller;
4710 }
4711
4712 parentClass = method.$owner.superclass;
4713 methodName = method.$name;
4714
4715 if (!(methodName in parentClass)) {
4716 throw new Error("this.callParent() was called but there's no such method (" + methodName +
4717 ") found in the parent class (" + (Ext.getClassName(parentClass) || 'Object') + ")");
4718 }
4719 }
4720 //</debug>
4721
4722 return superMethod.apply(this, args || noArgs);
4723 },
4724
4725 /**
4726 * This method is used by an override to call the superclass method but bypass any
4727 * overridden method. This is often done to "patch" a method that contains a bug
4728 * but for whatever reason cannot be fixed directly.
4729 *
4730 * Consider:
4731 *
4732 * Ext.define('Ext.some.Class', {
4733 * method: function () {
4734 * console.log('Good');
4735 * }
4736 * });
4737 *
4738 * Ext.define('Ext.some.DerivedClass', {
4739 * method: function () {
4740 * console.log('Bad');
4741 *
4742 * // ... logic but with a bug ...
4743 *
4744 * this.callParent();
4745 * }
4746 * });
4747 *
4748 * To patch the bug in `DerivedClass.method`, the typical solution is to create an
4749 * override:
4750 *
4751 * Ext.define('App.paches.DerivedClass', {
4752 * override: 'Ext.some.DerivedClass',
4753 *
4754 * method: function () {
4755 * console.log('Fixed');
4756 *
4757 * // ... logic but with bug fixed ...
4758 *
4759 * this.callSuper();
4760 * }
4761 * });
4762 *
4763 * The patch method cannot use `callParent` to call the superclass `method` since
4764 * that would call the overridden method containing the bug. In other words, the
4765 * above patch would only produce "Fixed" then "Good" in the console log, whereas,
4766 * using `callParent` would produce "Fixed" then "Bad" then "Good".
4767 *
4768 * @protected
4769 * @param {Array/Arguments} args The arguments, either an array or the `arguments` object
4770 * from the current method, for example: `this.callSuper(arguments)`
4771 * @return {Object} Returns the result of calling the superclass method
4772 */
4773 callSuper: function(args) {
4774 var method,
4775 superMethod = (method = this.callSuper.caller) && ((method = method.$owner ? method : method.caller) &&
4776 method.$owner.superclass[method.$name]);
4777
4778 //<debug error>
4779 if (!superMethod) {
4780 method = this.callSuper.caller;
4781 var parentClass, methodName;
4782
4783 if (!method.$owner) {
4784 if (!method.caller) {
4785 throw new Error("Attempting to call a protected method from the public scope, which is not allowed");
4786 }
4787
4788 method = method.caller;
4789 }
4790
4791 parentClass = method.$owner.superclass;
4792 methodName = method.$name;
4793
4794 if (!(methodName in parentClass)) {
4795 throw new Error("this.callSuper() was called but there's no such method (" + methodName +
4796 ") found in the parent class (" + (Ext.getClassName(parentClass) || 'Object') + ")");
4797 }
4798 }
4799 //</debug>
4800
4801 return superMethod.apply(this, args || noArgs);
4802 },
4803
4804 /**
4805 * Call the original method that was previously overridden with {@link Ext.Base#override},
4806 *
4807 * This method is deprecated as {@link #callParent} does the same thing.
4808 *
4809 * Ext.define('My.Cat', {
4810 * constructor: function() {
4811 * alert("I'm a cat!");
4812 * }
4813 * });
4814 *
4815 * My.Cat.override({
4816 * constructor: function() {
4817 * alert("I'm going to be a cat!");
4818 *
4819 * var instance = this.callOverridden();
4820 *
4821 * alert("Meeeeoooowwww");
4822 *
4823 * return instance;
4824 * }
4825 * });
4826 *
4827 * var kitty = new My.Cat(); // alerts "I'm going to be a cat!"
4828 * // alerts "I'm a cat!"
4829 * // alerts "Meeeeoooowwww"
4830 *
4831 * @param {Array/Arguments} args The arguments, either an array or the `arguments` object
4832 * from the current method, for example: `this.callOverridden(arguments)`
4833 * @return {Object} Returns the result of calling the overridden method
4834 * @protected
4835 */
4836 callOverridden: function(args) {
4837 var method = this.callOverridden.caller;
4838 return method && method.$previous.apply(this, args || noArgs);
4839 },
4840
4841 /**
4842 * @property {Ext.Class} self
4843 *
4844 * Get the reference to the current class from which this object was instantiated. Unlike {@link Ext.Base#statics},
4845 * `this.self` is scope-dependent and it's meant to be used for dynamic inheritance. See {@link Ext.Base#statics}
4846 * for a detailed comparison
4847 *
4848 * Ext.define('My.Cat', {
4849 * statics: {
4850 * speciesName: 'Cat' // My.Cat.speciesName = 'Cat'
4851 * },
4852 *
4853 * constructor: function() {
4854 * alert(this.self.speciesName); // dependent on 'this'
4855 * },
4856 *
4857 * clone: function() {
4858 * return new this.self();
4859 * }
4860 * });
4861 *
4862 *
4863 * Ext.define('My.SnowLeopard', {
4864 * extend: 'My.Cat',
4865 * statics: {
4866 * speciesName: 'Snow Leopard' // My.SnowLeopard.speciesName = 'Snow Leopard'
4867 * }
4868 * });
4869 *
4870 * var cat = new My.Cat(); // alerts 'Cat'
4871 * var snowLeopard = new My.SnowLeopard(); // alerts 'Snow Leopard'
4872 *
4873 * var clone = snowLeopard.clone();
4874 * alert(Ext.getClassName(clone)); // alerts 'My.SnowLeopard'
4875 *
4876 * @protected
4877 */
4878 self: Base,
4879
4880 // Default constructor, simply returns `this`
4881 constructor: function() {
4882 return this;
4883 },
4884
4885 //<feature classSystem.config>
4886
4887 wasInstantiated: false,
4888
4889 /**
4890 * Initialize configuration for this class. a typical example:
4891 *
4892 * Ext.define('My.awesome.Class', {
4893 * // The default config
4894 * config: {
4895 * name: 'Awesome',
4896 * isAwesome: true
4897 * },
4898 *
4899 * constructor: function(config) {
4900 * this.initConfig(config);
4901 * }
4902 * });
4903 *
4904 * var awesome = new My.awesome.Class({
4905 * name: 'Super Awesome'
4906 * });
4907 *
4908 * alert(awesome.getName()); // 'Super Awesome'
4909 *
4910 * @protected
4911 * @param {Object} instanceConfig
4912 * @return {Object} mixins The mixin prototypes as key - value pairs
4913 */
4914 initConfig: function(instanceConfig) {
4915 //<debug>
4916 // if (instanceConfig && instanceConfig.breakOnInitConfig) {
4917 // debugger;
4918 // }
4919 //</debug>
4920 var configNameCache = Ext.Class.configNameCache,
4921 prototype = this.self.prototype,
4922 initConfigList = this.initConfigList,
4923 initConfigMap = this.initConfigMap,
4924 config = new this.configClass,
4925 defaultConfig = this.defaultConfig,
4926 i, ln, name, value, nameMap, getName;
4927
4928 this.initConfig = Ext.emptyFn;
4929
4930 this.initialConfig = instanceConfig || {};
4931
4932 if (instanceConfig) {
4933 Ext.merge(config, instanceConfig);
4934 }
4935
4936 this.config = config;
4937
4938 // Optimize initConfigList *once* per class based on the existence of apply* and update* methods
4939 // Happens only once during the first instantiation
4940 if (!prototype.hasOwnProperty('wasInstantiated')) {
4941 prototype.wasInstantiated = true;
4942
4943 for (i = 0,ln = initConfigList.length; i < ln; i++) {
4944 name = initConfigList[i];
4945 nameMap = configNameCache[name];
4946 value = defaultConfig[name];
4947
4948 if (!(nameMap.apply in prototype)
4949 && !(nameMap.update in prototype)
4950 && prototype[nameMap.set].$isDefault
4951 && typeof value != 'object') {
4952 prototype[nameMap.internal] = defaultConfig[name];
4953 initConfigMap[name] = false;
4954 Ext.Array.remove(initConfigList, name);
4955 i--;
4956 ln--;
4957 }
4958 }
4959 }
4960
4961 if (instanceConfig) {
4962 initConfigList = initConfigList.slice();
4963
4964 for (name in instanceConfig) {
4965 if (name in defaultConfig && !initConfigMap[name]) {
4966 initConfigList.push(name);
4967 }
4968 }
4969 }
4970
4971 // Point all getters to the initGetters
4972 for (i = 0,ln = initConfigList.length; i < ln; i++) {
4973 name = initConfigList[i];
4974 nameMap = configNameCache[name];
4975 this[nameMap.get] = this[nameMap.initGet];
4976 }
4977
4978 this.beforeInitConfig(config);
4979
4980 for (i = 0,ln = initConfigList.length; i < ln; i++) {
4981 name = initConfigList[i];
4982 nameMap = configNameCache[name];
4983 getName = nameMap.get;
4984
4985 if (this.hasOwnProperty(getName)) {
4986 this[nameMap.set].call(this, config[name]);
4987 delete this[getName];
4988 }
4989 }
4990
4991 return this;
4992 },
4993
4994 beforeInitConfig: Ext.emptyFn,
4995
4996 /**
4997 * @private
4998 */
4999 getCurrentConfig: function() {
5000 var defaultConfig = this.defaultConfig,
5001 configNameCache = Ext.Class.configNameCache,
5002 config = {},
5003 name, nameMap;
5004
5005 for (name in defaultConfig) {
5006 nameMap = configNameCache[name];
5007 config[name] = this[nameMap.get].call(this);
5008 }
5009
5010 return config;
5011 },
5012
5013 /**
5014 * @private
5015 */
5016 setConfig: function(config, applyIfNotSet) {
5017 if (!config) {
5018 return this;
5019 }
5020
5021 var configNameCache = Ext.Class.configNameCache,
5022 currentConfig = this.config,
5023 defaultConfig = this.defaultConfig,
5024 initialConfig = this.initialConfig,
5025 configList = [],
5026 name, i, ln, nameMap;
5027
5028 applyIfNotSet = Boolean(applyIfNotSet);
5029
5030 for (name in config) {
5031 if ((applyIfNotSet && (name in initialConfig))) {
5032 continue;
5033 }
5034
5035 currentConfig[name] = config[name];
5036
5037 if (name in defaultConfig) {
5038 configList.push(name);
5039 nameMap = configNameCache[name];
5040 this[nameMap.get] = this[nameMap.initGet];
5041 }
5042 }
5043
5044 for (i = 0,ln = configList.length; i < ln; i++) {
5045 name = configList[i];
5046 nameMap = configNameCache[name];
5047 this[nameMap.set].call(this, config[name]);
5048 delete this[nameMap.get];
5049 }
5050
5051 return this;
5052 },
5053
5054 set: function(name, value) {
5055 return this[Ext.Class.configNameCache[name].set].call(this, value);
5056 },
5057
5058 get: function(name) {
5059 return this[Ext.Class.configNameCache[name].get].call(this);
5060 },
5061
5062 /**
5063 * @private
5064 */
5065 getConfig: function(name) {
5066 return this[Ext.Class.configNameCache[name].get].call(this);
5067 },
5068
5069 /**
5070 * @private
5071 */
5072 hasConfig: function(name) {
5073 return (name in this.defaultConfig);
5074 },
5075
5076 /**
5077 * Returns the initial configuration passed to constructor.
5078 *
5079 * @param {String} [name] When supplied, value for particular configuration
5080 * option is returned, otherwise the full config object is returned.
5081 * @return {Object/Mixed}
5082 */
5083 getInitialConfig: function(name) {
5084 var config = this.config;
5085
5086 if (!name) {
5087 return config;
5088 }
5089 else {
5090 return config[name];
5091 }
5092 },
5093
5094 /**
5095 * @private
5096 */
5097 onConfigUpdate: function(names, callback, scope) {
5098 var self = this.self,
5099 //<debug>
5100 className = self.$className,
5101 //</debug>
5102 i, ln, name,
5103 updaterName, updater, newUpdater;
5104
5105 names = Ext.Array.from(names);
5106
5107 scope = scope || this;
5108
5109 for (i = 0,ln = names.length; i < ln; i++) {
5110 name = names[i];
5111 updaterName = 'update' + Ext.String.capitalize(name);
5112 updater = this[updaterName] || Ext.emptyFn;
5113 newUpdater = function() {
5114 updater.apply(this, arguments);
5115 scope[callback].apply(scope, arguments);
5116 };
5117 newUpdater.$name = updaterName;
5118 newUpdater.$owner = self;
5119 //<debug>
5120 newUpdater.displayName = className + '#' + updaterName;
5121 //</debug>
5122
5123 this[updaterName] = newUpdater;
5124 }
5125 },
5126 //</feature>
5127
5128 /**
5129 * @private
5130 * @param {String} name
5131 * @param {Mixed} value
5132 * @return {Mixed}
5133 */
5134 link: function(name, value) {
5135 this.$links = {};
5136 this.link = this.doLink;
5137 return this.link.apply(this, arguments);
5138 },
5139
5140 doLink: function(name, value) {
5141 this.$links[name] = true;
5142
5143 this[name] = value;
5144
5145 return value;
5146 },
5147
5148 /**
5149 * @private
5150 */
5151 unlink: function() {
5152 var i, ln, link, value;
5153
5154 for (i = 0, ln = arguments.length; i < ln; i++) {
5155 link = arguments[i];
5156 if (this.hasOwnProperty(link)) {
5157 value = this[link];
5158 if (value) {
5159 if (value.isInstance && !value.isDestroyed) {
5160 value.destroy();
5161 }
5162 else if (value.parentNode && 'nodeType' in value) {
5163 value.parentNode.removeChild(value);
5164 }
5165 }
5166 delete this[link];
5167 }
5168 }
5169
5170 return this;
5171 },
5172
5173 /**
5174 * @protected
5175 */
5176 destroy: function() {
5177 this.destroy = Ext.emptyFn;
5178 this.isDestroyed = true;
5179
5180 if (this.hasOwnProperty('$links')) {
5181 this.unlink.apply(this, Ext.Object.getKeys(this.$links));
5182 delete this.$links;
5183 }
5184 }
5185 });
5186
5187 Ext.Base = Base;
5188
5189 })(Ext.Function.flexSetter);
5190
5191 //@tag foundation,core
5192 //@define Ext.Class
5193 //@require Ext.Base
5194
5195 /**
5196 * @class Ext.Class
5197 * @author Jacky Nguyen <jacky@sencha.com>
5198 *
5199 * Handles class creation throughout the framework. This is a low level factory that is used by Ext.ClassManager and generally
5200 * should not be used directly. If you choose to use Ext.Class you will lose out on the namespace, aliasing and dependency loading
5201 * features made available by Ext.ClassManager. The only time you would use Ext.Class directly is to create an anonymous class.
5202 *
5203 * If you wish to create a class you should use {@link Ext#define Ext.define} which aliases
5204 * {@link Ext.ClassManager#create Ext.ClassManager.create} to enable namespacing and dynamic dependency resolution.
5205 *
5206 * Ext.Class is the factory and **not** the superclass of everything. For the base class that **all** Ext classes inherit
5207 * from, see {@link Ext.Base}.
5208 *
5209 * For more information about Sencha Touch's Class System, please check out the [class system guide](../../../core_concepts/class_system.html).
5210 */
5211 (function() {
5212 var ExtClass,
5213 Base = Ext.Base,
5214 baseStaticMembers = [],
5215 baseStaticMember, baseStaticMemberLength;
5216
5217 for (baseStaticMember in Base) {
5218 if (Base.hasOwnProperty(baseStaticMember)) {
5219 baseStaticMembers.push(baseStaticMember);
5220 }
5221 }
5222
5223 baseStaticMemberLength = baseStaticMembers.length;
5224
5225 /**
5226 * @method constructor
5227 * Creates a new anonymous class.
5228 *
5229 * @param {Object} data An object represent the properties of this class.
5230 * @param {Function} onCreated (optional) The callback function to be executed when this class is fully created.
5231 * Note that the creation process can be asynchronous depending on the pre-processors used.
5232 *
5233 * @return {Ext.Base} The newly created class
5234 */
5235 Ext.Class = ExtClass = function(Class, data, onCreated) {
5236 if (typeof Class != 'function') {
5237 onCreated = data;
5238 data = Class;
5239 Class = null;
5240 }
5241
5242 if (!data) {
5243 data = {};
5244 }
5245
5246 Class = ExtClass.create(Class);
5247
5248 ExtClass.process(Class, data, onCreated);
5249
5250 return Class;
5251 };
5252
5253 Ext.apply(ExtClass, {
5254 /**
5255 * @private
5256 * @static
5257 */
5258 onBeforeCreated: function(Class, data, hooks) {
5259 Class.addMembers(data);
5260
5261 hooks.onCreated.call(Class, Class);
5262 },
5263
5264 /**
5265 * @private
5266 * @static
5267 */
5268 create: function(Class) {
5269 var name, i;
5270
5271 if (!Class) {
5272 Class = function() {
5273 return this.constructor.apply(this, arguments);
5274 };
5275 }
5276
5277 for (i = 0; i < baseStaticMemberLength; i++) {
5278 name = baseStaticMembers[i];
5279 Class[name] = Base[name];
5280 }
5281
5282 return Class;
5283 },
5284
5285 /**
5286 * @private
5287 * @static
5288 */
5289 process: function(Class, data, onCreated) {
5290 var preprocessorStack = data.preprocessors || ExtClass.defaultPreprocessors,
5291 preprocessors = this.preprocessors,
5292 hooks = {
5293 onBeforeCreated: this.onBeforeCreated,
5294 onCreated: onCreated || Ext.emptyFn
5295 },
5296 index = 0,
5297 name, preprocessor, properties,
5298 i, ln, fn, property, process;
5299
5300 delete data.preprocessors;
5301
5302 process = function(Class, data, hooks) {
5303 fn = null;
5304
5305 while (fn === null) {
5306 name = preprocessorStack[index++];
5307
5308 if (name) {
5309 preprocessor = preprocessors[name];
5310 properties = preprocessor.properties;
5311
5312 if (properties === true) {
5313 fn = preprocessor.fn;
5314 }
5315 else {
5316 for (i = 0,ln = properties.length; i < ln; i++) {
5317 property = properties[i];
5318
5319 if (data.hasOwnProperty(property)) {
5320 fn = preprocessor.fn;
5321 break;
5322 }
5323 }
5324 }
5325 }
5326 else {
5327 hooks.onBeforeCreated.apply(this, arguments);
5328 return;
5329 }
5330 }
5331
5332 if (fn.call(this, Class, data, hooks, process) !== false) {
5333 process.apply(this, arguments);
5334 }
5335 };
5336
5337 process.call(this, Class, data, hooks);
5338 },
5339
5340 /**
5341 * @private
5342 * @static
5343 */
5344 preprocessors: {},
5345
5346 /**
5347 * Register a new pre-processor to be used during the class creation process.
5348 *
5349 * @private
5350 * @static
5351 * @param {String} name The pre-processor's name.
5352 * @param {Function} fn The callback function to be executed. Typical format:
5353 *
5354 * function(cls, data, fn) {
5355 * // Your code here
5356 *
5357 * // Execute this when the processing is finished.
5358 * // Asynchronous processing is perfectly OK
5359 * if (fn) {
5360 * fn.call(this, cls, data);
5361 * }
5362 * });
5363 *
5364 * @param {Function} fn.cls The created class.
5365 * @param {Object} fn.data The set of properties passed in {@link Ext.Class} constructor.
5366 * @param {Function} fn.fn The callback function that __must__ to be executed when this
5367 * pre-processor finishes, regardless of whether the processing is synchronous or
5368 * asynchronous.
5369 * @param {String[]} [properties]
5370 * @param {String} [position]
5371 * @param {Object} [relativeTo]
5372 * @return {Ext.Class} this
5373 */
5374 registerPreprocessor: function(name, fn, properties, position, relativeTo) {
5375 if (!position) {
5376 position = 'last';
5377 }
5378
5379 if (!properties) {
5380 properties = [name];
5381 }
5382
5383 this.preprocessors[name] = {
5384 name: name,
5385 properties: properties || false,
5386 fn: fn
5387 };
5388
5389 this.setDefaultPreprocessorPosition(name, position, relativeTo);
5390
5391 return this;
5392 },
5393
5394 /**
5395 * Retrieve a pre-processor callback function by its name, which has been registered before.
5396 *
5397 * @private
5398 * @static
5399 * @param {String} name
5400 * @return {Function} preprocessor
5401 */
5402 getPreprocessor: function(name) {
5403 return this.preprocessors[name];
5404 },
5405
5406 /**
5407 * @private
5408 * @static
5409 */
5410 getPreprocessors: function() {
5411 return this.preprocessors;
5412 },
5413
5414 /**
5415 * @private
5416 * @static
5417 */
5418 defaultPreprocessors: [],
5419
5420 /**
5421 * Retrieve the array stack of default pre-processors.
5422 * @private
5423 * @static
5424 * @return {Function} defaultPreprocessors
5425 */
5426 getDefaultPreprocessors: function() {
5427 return this.defaultPreprocessors;
5428 },
5429
5430 /**
5431 * Set the default array stack of default pre-processors.
5432 *
5433 * @private
5434 * @static
5435 * @param {Array} preprocessors
5436 * @return {Ext.Class} this
5437 */
5438 setDefaultPreprocessors: function(preprocessors) {
5439 this.defaultPreprocessors = Ext.Array.from(preprocessors);
5440
5441 return this;
5442 },
5443
5444 /**
5445 * Insert this pre-processor at a specific position in the stack, optionally relative to
5446 * any existing pre-processor. For example:
5447 *
5448 * Ext.Class.registerPreprocessor('debug', function(cls, data, fn) {
5449 * // Your code here
5450 *
5451 * if (fn) {
5452 * fn.call(this, cls, data);
5453 * }
5454 * }).insertDefaultPreprocessor('debug', 'last');
5455 *
5456 * @private
5457 * @static
5458 * @param {String} name The pre-processor name. Note that it needs to be registered with
5459 * {@link Ext.Class#registerPreprocessor registerPreprocessor} before this.
5460 * @param {String} offset The insertion position. Four possible values are:
5461 * 'first', 'last', or: 'before', 'after' (relative to the name provided in the third argument).
5462 * @param {String} relativeName
5463 * @return {Ext.Class} this
5464 */
5465 setDefaultPreprocessorPosition: function(name, offset, relativeName) {
5466 var defaultPreprocessors = this.defaultPreprocessors,
5467 index;
5468
5469 if (typeof offset == 'string') {
5470 if (offset === 'first') {
5471 defaultPreprocessors.unshift(name);
5472
5473 return this;
5474 }
5475 else if (offset === 'last') {
5476 defaultPreprocessors.push(name);
5477
5478 return this;
5479 }
5480
5481 offset = (offset === 'after') ? 1 : -1;
5482 }
5483
5484 index = Ext.Array.indexOf(defaultPreprocessors, relativeName);
5485
5486 if (index !== -1) {
5487 Ext.Array.splice(defaultPreprocessors, Math.max(0, index + offset), 0, name);
5488 }
5489
5490 return this;
5491 },
5492
5493 /**
5494 * @private
5495 * @static
5496 */
5497 configNameCache: {},
5498
5499 /**
5500 * @private
5501 * @static
5502 */
5503 getConfigNameMap: function(name) {
5504 var cache = this.configNameCache,
5505 map = cache[name],
5506 capitalizedName;
5507
5508 if (!map) {
5509 capitalizedName = name.charAt(0).toUpperCase() + name.substr(1);
5510
5511 map = cache[name] = {
5512 name: name,
5513 internal: '_' + name,
5514 initializing: 'is' + capitalizedName + 'Initializing',
5515 apply: 'apply' + capitalizedName,
5516 update: 'update' + capitalizedName,
5517 set: 'set' + capitalizedName,
5518 get: 'get' + capitalizedName,
5519 initGet: 'initGet' + capitalizedName,
5520 doSet : 'doSet' + capitalizedName,
5521 changeEvent: name.toLowerCase() + 'change'
5522 }
5523 }
5524
5525 return map;
5526 },
5527
5528 /**
5529 * @private
5530 * @static
5531 */
5532 generateSetter: function(nameMap) {
5533 var internalName = nameMap.internal,
5534 getName = nameMap.get,
5535 applyName = nameMap.apply,
5536 updateName = nameMap.update,
5537 setter;
5538
5539 setter = function(value) {
5540 var oldValue = this[internalName],
5541 applier = this[applyName],
5542 updater = this[updateName];
5543
5544 delete this[getName];
5545
5546 if (applier) {
5547 value = applier.call(this, value, oldValue);
5548 if (typeof value == 'undefined') {
5549 return this;
5550 }
5551 }
5552
5553 this[internalName] = value;
5554
5555 if (updater && value !== oldValue) {
5556 updater.call(this, value, oldValue);
5557 }
5558
5559 return this;
5560 };
5561
5562 setter.$isDefault = true;
5563
5564 return setter;
5565 },
5566
5567 /**
5568 * @private
5569 * @static
5570 */
5571 generateInitGetter: function(nameMap) {
5572 var name = nameMap.name,
5573 setName = nameMap.set,
5574 getName = nameMap.get,
5575 initializingName = nameMap.initializing;
5576
5577 return function() {
5578 this[initializingName] = true;
5579 delete this[getName];
5580
5581 this[setName].call(this, this.config[name]);
5582 delete this[initializingName];
5583
5584 return this[getName].apply(this, arguments);
5585 }
5586 },
5587
5588 /**
5589 * @private
5590 * @static
5591 */
5592 generateGetter: function(nameMap) {
5593 var internalName = nameMap.internal;
5594
5595 return function() {
5596 return this[internalName];
5597 }
5598 }
5599 });
5600
5601 /**
5602 * @cfg {String} extend
5603 * The parent class that this class extends. For example:
5604 *
5605 * @example
5606 * Ext.define('Person', {
5607 * say: function(text) {
5608 * alert(text);
5609 * }
5610 * });
5611 *
5612 * Ext.define('Developer', {
5613 * extend: 'Person',
5614 * say: function(text) {
5615 * this.callParent(["print " + text]);
5616 * }
5617 * });
5618 *
5619 * var person1 = Ext.create("Person");
5620 * person1.say("Bill");
5621 *
5622 * var developer1 = Ext.create("Developer");
5623 * developer1.say("Ted");
5624 */
5625 ExtClass.registerPreprocessor('extend', function(Class, data) {
5626 var Base = Ext.Base,
5627 extend = data.extend,
5628 Parent;
5629
5630 delete data.extend;
5631
5632 if (extend && extend !== Object) {
5633 Parent = extend;
5634 }
5635 else {
5636 Parent = Base;
5637 }
5638
5639 Class.extend(Parent);
5640
5641 Class.triggerExtended.apply(Class, arguments);
5642
5643 if (data.onClassExtended) {
5644 Class.onExtended(data.onClassExtended, Class);
5645 delete data.onClassExtended;
5646 }
5647
5648 }, true);
5649
5650 //<feature classSystem.statics>
5651 /**
5652 * @cfg {Object} statics
5653 * List of static methods for this class. For example:
5654 *
5655 * Ext.define('Computer', {
5656 * statics: {
5657 * factory: function(brand) {
5658 * // 'this' in static methods refer to the class itself
5659 * return new this(brand);
5660 * }
5661 * },
5662 *
5663 * constructor: function() {
5664 * // ...
5665 * }
5666 * });
5667 *
5668 * var dellComputer = Computer.factory('Dell');
5669 */
5670 ExtClass.registerPreprocessor('statics', function(Class, data) {
5671 Class.addStatics(data.statics);
5672
5673 delete data.statics;
5674 });
5675 //</feature>
5676
5677 //<feature classSystem.inheritableStatics>
5678 /**
5679 * @cfg {Object} inheritableStatics
5680 * List of inheritable static methods for this class.
5681 * Otherwise just like {@link #statics} but subclasses inherit these methods.
5682 */
5683 ExtClass.registerPreprocessor('inheritableStatics', function(Class, data) {
5684 Class.addInheritableStatics(data.inheritableStatics);
5685
5686 delete data.inheritableStatics;
5687 });
5688 //</feature>
5689
5690 //<feature classSystem.platformConfig>
5691 /**
5692 * @cfg {Object} platformConfig
5693 * Allows for setting default config values on specific platforms or themes
5694 *
5695 * Ext.define('MyComponent', {
5696 * config: {
5697 * top: 0
5698 * },
5699 *
5700 * platformConfig: [{
5701 * platform: ['ie10'],
5702 * theme: ['Windows'],
5703 * top: null,
5704 * bottom: 0
5705 * }]
5706 * });
5707 */
5708 ExtClass.registerPreprocessor('platformConfig', function(Class, data, hooks) {
5709 var platformConfigs = data.platformConfig,
5710 config = data.config || {},
5711 platform, theme, platformConfig, i, ln, j, ln2, exclude;
5712
5713 delete data.platformConfig;
5714
5715 if (!Ext.filterPlatform) {
5716 Ext.filterPlatform = function(platform) {
5717 var profileMatch = false,
5718 ua = navigator.userAgent,
5719 j, jln;
5720
5721 platform = [].concat(platform);
5722
5723 function isPhone(ua) {
5724 var isMobile = /Mobile(\/|\s)/.test(ua);
5725
5726 // Either:
5727 // - iOS but not iPad
5728 // - Android 2
5729 // - Android with "Mobile" in the UA
5730
5731 return /(iPhone|iPod)/.test(ua) ||
5732 (!/(Silk)/.test(ua) && (/(Android)/.test(ua) && (/(Android 2)/.test(ua) || isMobile))) ||
5733 (/(BlackBerry|BB)/.test(ua) && isMobile) ||
5734 /(Windows Phone)/.test(ua);
5735 }
5736
5737 function isTablet(ua) {
5738 return !isPhone(ua) && (/iPad/.test(ua) || /Android/.test(ua) || /(RIM Tablet OS)/.test(ua) ||
5739 (/MSIE 10/.test(ua) && /; Touch/.test(ua)));
5740 }
5741
5742 // Check if the ?platform parameter is set in the URL
5743 var paramsString = window.location.search.substr(1),
5744 paramsArray = paramsString.split("&"),
5745 params = {},
5746 testPlatform, i;
5747
5748 for (i = 0; i < paramsArray.length; i++) {
5749 var tmpArray = paramsArray[i].split("=");
5750 params[tmpArray[0]] = tmpArray[1];
5751 }
5752
5753 testPlatform = params.platform;
5754 if (testPlatform) {
5755 return platform.indexOf(testPlatform) != -1;
5756 }
5757
5758 for (j = 0, jln = platform.length; j < jln; j++) {
5759 switch (platform[j]) {
5760 case 'phone':
5761 profileMatch = isPhone(ua);
5762 break;
5763 case 'tablet':
5764 profileMatch = isTablet(ua);
5765 break;
5766 case 'desktop':
5767 profileMatch = !isPhone(ua) && !isTablet(ua);
5768 break;
5769 case 'ios':
5770 profileMatch = /(iPad|iPhone|iPod)/.test(ua);
5771 break;
5772 case 'android':
5773 profileMatch = /(Android|Silk)/.test(ua);
5774 break;
5775 case 'blackberry':
5776 profileMatch = /(BlackBerry|BB)/.test(ua);
5777 break;
5778 case 'safari':
5779 profileMatch = /Safari/.test(ua) && !(/(BlackBerry|BB)/.test(ua));
5780 break;
5781 case 'chrome':
5782 profileMatch = /Chrome/.test(ua);
5783 break;
5784 case 'ie10':
5785 profileMatch = /MSIE 10/.test(ua);
5786 break;
5787 case 'windows':
5788 profileMatch = /MSIE 10/.test(ua) || /Trident/.test(ua);
5789 break;
5790 case 'tizen':
5791 profileMatch = /Tizen/.test(ua);
5792 break;
5793 case 'firefox':
5794 profileMatch = /Firefox/.test(ua);
5795 }
5796 if (profileMatch) {
5797 return true;
5798 }
5799 }
5800 return false;
5801 };
5802 }
5803
5804 for (i = 0, ln = platformConfigs.length; i < ln; i++) {
5805 platformConfig = platformConfigs[i];
5806
5807 platform = platformConfig.platform;
5808 exclude = platformConfig.exclude || [];
5809 delete platformConfig.platform;
5810
5811 theme = [].concat(platformConfig.theme);
5812 ln2 = theme.length;
5813 delete platformConfig.theme;
5814
5815 if (platform && Ext.filterPlatform(platform) && !Ext.filterPlatform(exclude)) {
5816 Ext.merge(config, platformConfig);
5817 }
5818
5819 if (ln2) {
5820 for (j = 0; j < ln2; j++) {
5821 if (Ext.theme.name == theme[j]) {
5822 Ext.merge(config, platformConfig);
5823 }
5824 }
5825 }
5826 }
5827 });
5828 //</feature>
5829
5830 //<feature classSystem.config>
5831 /**
5832 * @cfg {Object} config
5833 *
5834 * List of configuration options with their default values.
5835 *
5836 * __Note:__ You need to make sure {@link Ext.Base#initConfig} is called from your constructor if you are defining
5837 * your own class or singleton, unless you are extending a Component. Otherwise the generated getter and setter
5838 * methods will not be initialized.
5839 *
5840 * Each config item will have its own setter and getter method automatically generated inside the class prototype
5841 * during class creation time, if the class does not have those methods explicitly defined.
5842 *
5843 * As an example, let's convert the name property of a Person class to be a config item, then add extra age and
5844 * gender items.
5845 *
5846 * Ext.define('My.sample.Person', {
5847 * config: {
5848 * name: 'Mr. Unknown',
5849 * age: 0,
5850 * gender: 'Male'
5851 * },
5852 *
5853 * constructor: function(config) {
5854 * this.initConfig(config);
5855 *
5856 * return this;
5857 * }
5858 *
5859 * // ...
5860 * });
5861 *
5862 * Within the class, this.name still has the default value of "Mr. Unknown". However, it's now publicly accessible
5863 * without sacrificing encapsulation, via setter and getter methods.
5864 *
5865 * var jacky = new Person({
5866 * name: "Jacky",
5867 * age: 35
5868 * });
5869 *
5870 * alert(jacky.getAge()); // alerts 35
5871 * alert(jacky.getGender()); // alerts "Male"
5872 *
5873 * jacky.walk(10); // alerts "Jacky is walking 10 steps"
5874 *
5875 * jacky.setName("Mr. Nguyen");
5876 * alert(jacky.getName()); // alerts "Mr. Nguyen"
5877 *
5878 * jacky.walk(10); // alerts "Mr. Nguyen is walking 10 steps"
5879 *
5880 * Notice that we changed the class constructor to invoke this.initConfig() and pass in the provided config object.
5881 * Two key things happened:
5882 *
5883 * - The provided config object when the class is instantiated is recursively merged with the default config object.
5884 * - All corresponding setter methods are called with the merged values.
5885 *
5886 * Beside storing the given values, throughout the frameworks, setters generally have two key responsibilities:
5887 *
5888 * - Filtering / validation / transformation of the given value before it's actually stored within the instance.
5889 * - Notification (such as firing events) / post-processing after the value has been set, or changed from a
5890 * previous value.
5891 *
5892 * By standardize this common pattern, the default generated setters provide two extra template methods that you
5893 * can put your own custom logics into, i.e: an "applyFoo" and "updateFoo" method for a "foo" config item, which are
5894 * executed before and after the value is actually set, respectively. Back to the example class, let's validate that
5895 * age must be a valid positive number, and fire an 'agechange' if the value is modified.
5896 *
5897 * Ext.define('My.sample.Person', {
5898 * config: {
5899 * // ...
5900 * },
5901 *
5902 * constructor: {
5903 * // ...
5904 * },
5905 *
5906 * applyAge: function(age) {
5907 * if (typeof age !== 'number' || age < 0) {
5908 * console.warn("Invalid age, must be a positive number");
5909 * return;
5910 * }
5911 *
5912 * return age;
5913 * },
5914 *
5915 * updateAge: function(newAge, oldAge) {
5916 * // age has changed from "oldAge" to "newAge"
5917 * this.fireEvent('agechange', this, newAge, oldAge);
5918 * }
5919 *
5920 * // ...
5921 * });
5922 *
5923 * var jacky = new Person({
5924 * name: "Jacky",
5925 * age: 'invalid'
5926 * });
5927 *
5928 * alert(jacky.getAge()); // alerts 0
5929 *
5930 * alert(jacky.setAge(-100)); // alerts 0
5931 * alert(jacky.getAge()); // alerts 0
5932 *
5933 * alert(jacky.setAge(35)); // alerts 0
5934 * alert(jacky.getAge()); // alerts 35
5935 *
5936 * In other words, when leveraging the config feature, you mostly never need to define setter and getter methods
5937 * explicitly. Instead, "apply*" and "update*" methods should be implemented where necessary. Your code will be
5938 * consistent throughout and only contain the minimal logic that you actually care about.
5939 *
5940 * When it comes to inheritance, the default config of the parent class is automatically, recursively merged with
5941 * the child's default config. The same applies for mixins.
5942 */
5943 ExtClass.registerPreprocessor('config', function(Class, data) {
5944 var config = data.config,
5945 prototype = Class.prototype,
5946 defaultConfig = prototype.config,
5947 nameMap, name, setName, getName, initGetName, internalName, value;
5948
5949 delete data.config;
5950
5951 for (name in config) {
5952 // Once per config item, per class hierarchy
5953 if (config.hasOwnProperty(name) && !(name in defaultConfig)) {
5954 value = config[name];
5955 nameMap = this.getConfigNameMap(name);
5956 setName = nameMap.set;
5957 getName = nameMap.get;
5958 initGetName = nameMap.initGet;
5959 internalName = nameMap.internal;
5960
5961 data[initGetName] = this.generateInitGetter(nameMap);
5962
5963 if (value === null && !data.hasOwnProperty(internalName)) {
5964 data[internalName] = null;
5965 }
5966
5967 if (!data.hasOwnProperty(getName)) {
5968 data[getName] = this.generateGetter(nameMap);
5969 }
5970
5971 if (!data.hasOwnProperty(setName)) {
5972 data[setName] = this.generateSetter(nameMap);
5973 }
5974 }
5975 }
5976
5977 Class.addConfig(config, true);
5978 });
5979 //</feature>
5980
5981 //<feature classSystem.mixins>
5982 /**
5983 * @cfg {Object} mixins
5984 * List of classes to mix into this class. For example:
5985 *
5986 * Ext.define('CanSing', {
5987 * sing: function() {
5988 * alert("I'm on the highway to hell...");
5989 * }
5990 * });
5991 *
5992 * Ext.define('Musician', {
5993 * extend: 'Person',
5994 *
5995 * mixins: {
5996 * canSing: 'CanSing'
5997 * }
5998 * });
5999 */
6000 ExtClass.registerPreprocessor('mixins', function(Class, data, hooks) {
6001 var mixins = data.mixins,
6002 name, mixin, i, ln;
6003
6004 delete data.mixins;
6005
6006 Ext.Function.interceptBefore(hooks, 'onCreated', function() {
6007 if (mixins instanceof Array) {
6008 for (i = 0,ln = mixins.length; i < ln; i++) {
6009 mixin = mixins[i];
6010 name = mixin.prototype.mixinId || mixin.$className;
6011
6012 Class.mixin(name, mixin);
6013 }
6014 }
6015 else {
6016 for (name in mixins) {
6017 if (mixins.hasOwnProperty(name)) {
6018 Class.mixin(name, mixins[name]);
6019 }
6020 }
6021 }
6022 });
6023 });
6024 //</feature>
6025
6026 //<feature classSystem.backwardsCompatible>
6027 // Backwards compatible
6028 Ext.extend = function(Class, Parent, members) {
6029 if (arguments.length === 2 && Ext.isObject(Parent)) {
6030 members = Parent;
6031 Parent = Class;
6032 Class = null;
6033 }
6034
6035 var cls;
6036
6037 if (!Parent) {
6038 throw new Error("[Ext.extend] Attempting to extend from a class which has not been loaded on the page.");
6039 }
6040
6041 members.extend = Parent;
6042 members.preprocessors = [
6043 'extend'
6044
6045 //<feature classSystem.statics>
6046 ,'statics'
6047 //</feature>
6048
6049 //<feature classSystem.inheritableStatics>
6050 ,'inheritableStatics'
6051 //</feature>
6052
6053 //<feature classSystem.mixins>
6054 ,'mixins'
6055 //</feature>
6056
6057 //<feature classSystem.platformConfig>
6058 ,'platformConfig'
6059 //</feature>
6060
6061 //<feature classSystem.config>
6062 ,'config'
6063 //</feature>
6064 ];
6065
6066 if (Class) {
6067 cls = new ExtClass(Class, members);
6068 }
6069 else {
6070 cls = new ExtClass(members);
6071 }
6072
6073 cls.prototype.override = function(o) {
6074 for (var m in o) {
6075 if (o.hasOwnProperty(m)) {
6076 this[m] = o[m];
6077 }
6078 }
6079 };
6080
6081 return cls;
6082 };
6083 //</feature>
6084 })();
6085
6086 //@tag foundation,core
6087 //@define Ext.ClassManager
6088 //@require Ext.Class
6089
6090 /**
6091 * @class Ext.ClassManager
6092 * @author Jacky Nguyen <jacky@sencha.com>
6093 *
6094 * Ext.ClassManager manages all classes and handles mapping from string class name to
6095 * actual class objects throughout the whole framework. It is not generally accessed directly, rather through
6096 * these convenient shorthands:
6097 *
6098 * - {@link Ext#define Ext.define}
6099 * - {@link Ext.ClassManager#create Ext.create}
6100 * - {@link Ext#widget Ext.widget}
6101 * - {@link Ext#getClass Ext.getClass}
6102 * - {@link Ext#getClassName Ext.getClassName}
6103 *
6104 * For more information about Sencha Touch's Class System, please check out the [class system guide](../../../core_concepts/class_system.html).
6105 *
6106 * ## Basic syntax:
6107 *
6108 * Ext.define(className, properties);
6109 *
6110 * in which `properties` is an object represent a collection of properties that apply to the class. See
6111 * {@link Ext.ClassManager#create} for more detailed instructions.
6112 *
6113 * @example
6114 * Ext.define('Person', {
6115 * name: 'Unknown',
6116 *
6117 * constructor: function(name) {
6118 * if (name) {
6119 * this.name = name;
6120 * }
6121 *
6122 * return this;
6123 * },
6124 *
6125 * eat: function(foodType) {
6126 * alert("I'm eating: " + foodType);
6127 *
6128 * return this;
6129 * }
6130 * });
6131 *
6132 * var aaron = new Person("Aaron");
6133 * aaron.eat("Sandwich"); // alert("I'm eating: Sandwich");
6134 *
6135 * Ext.Class has a powerful set of extensible {@link Ext.Class#registerPreprocessor pre-processors} which takes care of
6136 * everything related to class creation, including but not limited to inheritance, mixins, configuration, statics, etc.
6137 *
6138 * ## Inheritance:
6139 *
6140 * Ext.define('Developer', {
6141 * extend: 'Person',
6142 *
6143 * constructor: function(name, isGeek) {
6144 * this.isGeek = isGeek;
6145 *
6146 * // Apply a method from the parent class' prototype
6147 * this.callParent([name]);
6148 *
6149 * return this;
6150 *
6151 * },
6152 *
6153 * code: function(language) {
6154 * alert("I'm coding in: " + language);
6155 *
6156 * this.eat("Bugs");
6157 *
6158 * return this;
6159 * }
6160 * });
6161 *
6162 * var jacky = new Developer("Jacky", true);
6163 * jacky.code("JavaScript"); // alert("I'm coding in: JavaScript");
6164 * // alert("I'm eating: Bugs");
6165 *
6166 * See {@link Ext.Base#callParent} for more details on calling superclass' methods
6167 *
6168 * ## Mixins:
6169 *
6170 * Ext.define('CanPlayGuitar', {
6171 * playGuitar: function() {
6172 * alert("F#...G...D...A");
6173 * }
6174 * });
6175 *
6176 * Ext.define('CanComposeSongs', {
6177 * composeSongs: function() { }
6178 * });
6179 *
6180 * Ext.define('CanSing', {
6181 * sing: function() {
6182 * alert("I'm on the highway to hell...");
6183 * }
6184 * });
6185 *
6186 * Ext.define('Musician', {
6187 * extend: 'Person',
6188 *
6189 * mixins: {
6190 * canPlayGuitar: 'CanPlayGuitar',
6191 * canComposeSongs: 'CanComposeSongs',
6192 * canSing: 'CanSing'
6193 * }
6194 * });
6195 *
6196 * Ext.define('CoolPerson', {
6197 * extend: 'Person',
6198 *
6199 * mixins: {
6200 * canPlayGuitar: 'CanPlayGuitar',
6201 * canSing: 'CanSing'
6202 * },
6203 *
6204 * sing: function() {
6205 * alert("Ahem...");
6206 *
6207 * this.mixins.canSing.sing.call(this);
6208 *
6209 * alert("[Playing guitar at the same time...]");
6210 *
6211 * this.playGuitar();
6212 * }
6213 * });
6214 *
6215 * var me = new CoolPerson("Jacky");
6216 *
6217 * me.sing(); // alert("Ahem...");
6218 * // alert("I'm on the highway to hell...");
6219 * // alert("[Playing guitar at the same time...]");
6220 * // alert("F#...G...D...A");
6221 *
6222 * ## Config:
6223 *
6224 * Ext.define('SmartPhone', {
6225 * config: {
6226 * hasTouchScreen: false,
6227 * operatingSystem: 'Other',
6228 * price: 500
6229 * },
6230 *
6231 * isExpensive: false,
6232 *
6233 * constructor: function(config) {
6234 * this.initConfig(config);
6235 *
6236 * return this;
6237 * },
6238 *
6239 * applyPrice: function(price) {
6240 * this.isExpensive = (price > 500);
6241 *
6242 * return price;
6243 * },
6244 *
6245 * applyOperatingSystem: function(operatingSystem) {
6246 * if (!(/^(iOS|Android|BlackBerry)$/i).test(operatingSystem)) {
6247 * return 'Other';
6248 * }
6249 *
6250 * return operatingSystem;
6251 * }
6252 * });
6253 *
6254 * var iPhone = new SmartPhone({
6255 * hasTouchScreen: true,
6256 * operatingSystem: 'iOS'
6257 * });
6258 *
6259 * iPhone.getPrice(); // 500;
6260 * iPhone.getOperatingSystem(); // 'iOS'
6261 * iPhone.getHasTouchScreen(); // true;
6262 *
6263 * iPhone.isExpensive; // false;
6264 * iPhone.setPrice(600);
6265 * iPhone.getPrice(); // 600
6266 * iPhone.isExpensive; // true;
6267 *
6268 * iPhone.setOperatingSystem('AlienOS');
6269 * iPhone.getOperatingSystem(); // 'Other'
6270 *
6271 * ## Statics:
6272 *
6273 * Ext.define('Computer', {
6274 * statics: {
6275 * factory: function(brand) {
6276 * // 'this' in static methods refer to the class itself
6277 * return new this(brand);
6278 * }
6279 * },
6280 *
6281 * constructor: function() { }
6282 * });
6283 *
6284 * var dellComputer = Computer.factory('Dell');
6285 *
6286 * Also see {@link Ext.Base#statics} and {@link Ext.Base#self} for more details on accessing
6287 * static properties within class methods
6288 *
6289 * @singleton
6290 */
6291 (function(Class, alias, arraySlice, arrayFrom, global) {
6292 var Manager = Ext.ClassManager = {
6293
6294 /**
6295 * @property classes
6296 * @type Object
6297 * All classes which were defined through the ClassManager. Keys are the
6298 * name of the classes and the values are references to the classes.
6299 * @private
6300 */
6301 classes: {},
6302
6303 /**
6304 * @private
6305 */
6306 existCache: {},
6307
6308 /**
6309 * @private
6310 */
6311 namespaceRewrites: [{
6312 from: 'Ext.',
6313 to: Ext
6314 }],
6315
6316 /**
6317 * @private
6318 */
6319 maps: {
6320 alternateToName: {},
6321 aliasToName: {},
6322 nameToAliases: {},
6323 nameToAlternates: {}
6324 },
6325
6326 /** @private */
6327 enableNamespaceParseCache: true,
6328
6329 /** @private */
6330 namespaceParseCache: {},
6331
6332 /** @private */
6333 instantiators: [],
6334
6335 /**
6336 * Checks if a class has already been created.
6337 *
6338 * @param {String} className
6339 * @return {Boolean} exist
6340 */
6341 isCreated: function(className) {
6342 var existCache = this.existCache,
6343 i, ln, part, root, parts;
6344
6345 //<debug error>
6346 if (typeof className != 'string' || className.length < 1) {
6347 throw new Error("[Ext.ClassManager] Invalid classname, must be a string and must not be empty");
6348 }
6349 //</debug>
6350
6351 if (this.classes[className] || existCache[className]) {
6352 return true;
6353 }
6354
6355 root = global;
6356 parts = this.parseNamespace(className);
6357
6358 for (i = 0, ln = parts.length; i < ln; i++) {
6359 part = parts[i];
6360
6361 if (typeof part != 'string') {
6362 root = part;
6363 } else {
6364 if (!root || !root[part]) {
6365 return false;
6366 }
6367
6368 root = root[part];
6369 }
6370 }
6371
6372 existCache[className] = true;
6373
6374 this.triggerCreated(className);
6375
6376 return true;
6377 },
6378
6379 /**
6380 * @private
6381 */
6382 createdListeners: [],
6383
6384 /**
6385 * @private
6386 */
6387 nameCreatedListeners: {},
6388
6389 /**
6390 * @private
6391 */
6392 triggerCreated: function(className) {
6393 var listeners = this.createdListeners,
6394 nameListeners = this.nameCreatedListeners,
6395 alternateNames = this.maps.nameToAlternates[className],
6396 names = [className],
6397 i, ln, j, subLn, listener, name;
6398
6399 for (i = 0,ln = listeners.length; i < ln; i++) {
6400 listener = listeners[i];
6401 listener.fn.call(listener.scope, className);
6402 }
6403
6404 if (alternateNames) {
6405 names.push.apply(names, alternateNames);
6406 }
6407
6408 for (i = 0,ln = names.length; i < ln; i++) {
6409 name = names[i];
6410 listeners = nameListeners[name];
6411
6412 if (listeners) {
6413 for (j = 0,subLn = listeners.length; j < subLn; j++) {
6414 listener = listeners[j];
6415 listener.fn.call(listener.scope, name);
6416 }
6417 delete nameListeners[name];
6418 }
6419 }
6420 },
6421
6422 /**
6423 * @private
6424 */
6425 onCreated: function(fn, scope, className) {
6426 var listeners = this.createdListeners,
6427 nameListeners = this.nameCreatedListeners,
6428 listener = {
6429 fn: fn,
6430 scope: scope
6431 };
6432
6433 if (className) {
6434 if (this.isCreated(className)) {
6435 fn.call(scope, className);
6436 return;
6437 }
6438
6439 if (!nameListeners[className]) {
6440 nameListeners[className] = [];
6441 }
6442
6443 nameListeners[className].push(listener);
6444 }
6445 else {
6446 listeners.push(listener);
6447 }
6448 },
6449
6450 /**
6451 * Supports namespace rewriting.
6452 * @private
6453 */
6454 parseNamespace: function(namespace) {
6455 //<debug error>
6456 if (typeof namespace != 'string') {
6457 throw new Error("[Ext.ClassManager] Invalid namespace, must be a string");
6458 }
6459 //</debug>
6460
6461 var cache = this.namespaceParseCache;
6462
6463 if (this.enableNamespaceParseCache) {
6464 if (cache.hasOwnProperty(namespace)) {
6465 return cache[namespace];
6466 }
6467 }
6468
6469 var parts = [],
6470 rewrites = this.namespaceRewrites,
6471 root = global,
6472 name = namespace,
6473 rewrite, from, to, i, ln;
6474
6475 for (i = 0, ln = rewrites.length; i < ln; i++) {
6476 rewrite = rewrites[i];
6477 from = rewrite.from;
6478 to = rewrite.to;
6479
6480 if (name === from || name.substring(0, from.length) === from) {
6481 name = name.substring(from.length);
6482
6483 if (typeof to != 'string') {
6484 root = to;
6485 } else {
6486 parts = parts.concat(to.split('.'));
6487 }
6488
6489 break;
6490 }
6491 }
6492
6493 parts.push(root);
6494
6495 parts = parts.concat(name.split('.'));
6496
6497 if (this.enableNamespaceParseCache) {
6498 cache[namespace] = parts;
6499 }
6500
6501 return parts;
6502 },
6503
6504 /**
6505 * Creates a namespace and assign the `value` to the created object.
6506 *
6507 * Ext.ClassManager.setNamespace('MyCompany.pkg.Example', someObject);
6508 * alert(MyCompany.pkg.Example === someObject); // alerts true
6509 *
6510 * @param {String} name
6511 * @param {Mixed} value
6512 */
6513 setNamespace: function(name, value) {
6514 var root = global,
6515 parts = this.parseNamespace(name),
6516 ln = parts.length - 1,
6517 leaf = parts[ln],
6518 i, part;
6519
6520 for (i = 0; i < ln; i++) {
6521 part = parts[i];
6522
6523 if (typeof part != 'string') {
6524 root = part;
6525 } else {
6526 if (!root[part]) {
6527 root[part] = {};
6528 }
6529
6530 root = root[part];
6531 }
6532 }
6533
6534 root[leaf] = value;
6535
6536 return root[leaf];
6537 },
6538
6539 /**
6540 * The new Ext.ns, supports namespace rewriting.
6541 * @private
6542 */
6543 createNamespaces: function() {
6544 var root = global,
6545 parts, part, i, j, ln, subLn;
6546
6547 for (i = 0, ln = arguments.length; i < ln; i++) {
6548 parts = this.parseNamespace(arguments[i]);
6549
6550 for (j = 0, subLn = parts.length; j < subLn; j++) {
6551 part = parts[j];
6552
6553 if (typeof part != 'string') {
6554 root = part;
6555 } else {
6556 if (!root[part]) {
6557 root[part] = {};
6558 }
6559
6560 root = root[part];
6561 }
6562 }
6563 }
6564
6565 return root;
6566 },
6567
6568 /**
6569 * Sets a name reference to a class.
6570 *
6571 * @param {String} name
6572 * @param {Object} value
6573 * @return {Ext.ClassManager} this
6574 */
6575 set: function(name, value) {
6576 var me = this,
6577 maps = me.maps,
6578 nameToAlternates = maps.nameToAlternates,
6579 targetName = me.getName(value),
6580 alternates;
6581
6582 me.classes[name] = me.setNamespace(name, value);
6583
6584 if (targetName && targetName !== name) {
6585 maps.alternateToName[name] = targetName;
6586 alternates = nameToAlternates[targetName] || (nameToAlternates[targetName] = []);
6587 alternates.push(name);
6588 }
6589
6590 return this;
6591 },
6592
6593 /**
6594 * Retrieve a class by its name.
6595 *
6596 * @param {String} name
6597 * @return {Ext.Class} class
6598 */
6599 get: function(name) {
6600 var classes = this.classes;
6601
6602 if (classes[name]) {
6603 return classes[name];
6604 }
6605
6606 var root = global,
6607 parts = this.parseNamespace(name),
6608 part, i, ln;
6609
6610 for (i = 0, ln = parts.length; i < ln; i++) {
6611 part = parts[i];
6612
6613 if (typeof part != 'string') {
6614 root = part;
6615 } else {
6616 if (!root || !root[part]) {
6617 return null;
6618 }
6619
6620 root = root[part];
6621 }
6622 }
6623
6624 return root;
6625 },
6626
6627 /**
6628 * Register the alias for a class.
6629 *
6630 * @param {Ext.Class/String} cls a reference to a class or a `className`.
6631 * @param {String} alias Alias to use when referring to this class.
6632 */
6633 setAlias: function(cls, alias) {
6634 var aliasToNameMap = this.maps.aliasToName,
6635 nameToAliasesMap = this.maps.nameToAliases,
6636 className;
6637
6638 if (typeof cls == 'string') {
6639 className = cls;
6640 } else {
6641 className = this.getName(cls);
6642 }
6643
6644 if (alias && aliasToNameMap[alias] !== className) {
6645 //<debug info>
6646 if (aliasToNameMap[alias]) {
6647 Ext.Logger.info("[Ext.ClassManager] Overriding existing alias: '" + alias + "' " +
6648 "of: '" + aliasToNameMap[alias] + "' with: '" + className + "'. Be sure it's intentional.");
6649 }
6650 //</debug>
6651
6652 aliasToNameMap[alias] = className;
6653 }
6654
6655 if (!nameToAliasesMap[className]) {
6656 nameToAliasesMap[className] = [];
6657 }
6658
6659 if (alias) {
6660 Ext.Array.include(nameToAliasesMap[className], alias);
6661 }
6662
6663 return this;
6664 },
6665
6666 /**
6667 * Adds a batch of class name to alias mappings
6668 * @param {Object} aliases The set of mappings of the form
6669 * className : [values...]
6670 */
6671 addNameAliasMappings: function(aliases){
6672 var aliasToNameMap = this.maps.aliasToName,
6673 nameToAliasesMap = this.maps.nameToAliases,
6674 className, aliasList, alias, i;
6675
6676 for (className in aliases) {
6677 aliasList = nameToAliasesMap[className] ||
6678 (nameToAliasesMap[className] = []);
6679
6680 for (i = 0; i < aliases[className].length; i++) {
6681 alias = aliases[className][i];
6682 if (!aliasToNameMap[alias]) {
6683 aliasToNameMap[alias] = className;
6684 aliasList.push(alias);
6685 }
6686 }
6687
6688 }
6689 return this;
6690 },
6691
6692 /**
6693 *
6694 * @param {Object} alternates The set of mappings of the form
6695 * className : [values...]
6696 */
6697 addNameAlternateMappings: function(alternates) {
6698 var alternateToName = this.maps.alternateToName,
6699 nameToAlternates = this.maps.nameToAlternates,
6700 className, aliasList, alternate, i;
6701
6702 for (className in alternates) {
6703 aliasList = nameToAlternates[className] ||
6704 (nameToAlternates[className] = []);
6705
6706 for (i = 0; i < alternates[className].length; i++) {
6707 alternate = alternates[className];
6708 if (!alternateToName[alternate]) {
6709 alternateToName[alternate] = className;
6710 aliasList.push(alternate);
6711 }
6712 }
6713
6714 }
6715 return this;
6716 },
6717
6718 /**
6719 * Get a reference to the class by its alias.
6720 *
6721 * @param {String} alias
6722 * @return {Ext.Class} class
6723 */
6724 getByAlias: function(alias) {
6725 return this.get(this.getNameByAlias(alias));
6726 },
6727
6728 /**
6729 * Get the name of a class by its alias.
6730 *
6731 * @param {String} alias
6732 * @return {String} className
6733 */
6734 getNameByAlias: function(alias) {
6735 return this.maps.aliasToName[alias] || '';
6736 },
6737
6738 /**
6739 * Get the name of a class by its alternate name.
6740 *
6741 * @param {String} alternate
6742 * @return {String} className
6743 */
6744 getNameByAlternate: function(alternate) {
6745 return this.maps.alternateToName[alternate] || '';
6746 },
6747
6748 /**
6749 * Get the aliases of a class by the class name
6750 *
6751 * @param {String} name
6752 * @return {Array} aliases
6753 */
6754 getAliasesByName: function(name) {
6755 return this.maps.nameToAliases[name] || [];
6756 },
6757
6758 /**
6759 * Get the name of the class by its reference or its instance;
6760 * usually invoked by the shorthand {@link Ext#getClassName Ext.getClassName}
6761 *
6762 * Ext.ClassManager.getName(Ext.Action); // returns "Ext.Action"
6763 *
6764 * @param {Ext.Class/Object} object
6765 * @return {String} className
6766 */
6767 getName: function(object) {
6768 return object && object.$className || '';
6769 },
6770
6771 /**
6772 * Get the class of the provided object; returns null if it's not an instance
6773 * of any class created with Ext.define. This is usually invoked by the shorthand {@link Ext#getClass Ext.getClass}.
6774 *
6775 * var component = new Ext.Component();
6776 *
6777 * Ext.ClassManager.getClass(component); // returns Ext.Component
6778 *
6779 * @param {Object} object
6780 * @return {Ext.Class} class
6781 */
6782 getClass: function(object) {
6783 return object && object.self || null;
6784 },
6785
6786 /**
6787 * @private
6788 */
6789 create: function(className, data, createdFn) {
6790 //<debug error>
6791 if (typeof className != 'string') {
6792 throw new Error("[Ext.define] Invalid class name '" + className + "' specified, must be a non-empty string");
6793 }
6794 //</debug>
6795
6796 data.$className = className;
6797
6798 return new Class(data, function() {
6799 var postprocessorStack = data.postprocessors || Manager.defaultPostprocessors,
6800 registeredPostprocessors = Manager.postprocessors,
6801 index = 0,
6802 postprocessors = [],
6803 postprocessor, process, i, ln, j, subLn, postprocessorProperties, postprocessorProperty;
6804
6805 delete data.postprocessors;
6806
6807 for (i = 0,ln = postprocessorStack.length; i < ln; i++) {
6808 postprocessor = postprocessorStack[i];
6809
6810 if (typeof postprocessor == 'string') {
6811 postprocessor = registeredPostprocessors[postprocessor];
6812 postprocessorProperties = postprocessor.properties;
6813
6814 if (postprocessorProperties === true) {
6815 postprocessors.push(postprocessor.fn);
6816 }
6817 else if (postprocessorProperties) {
6818 for (j = 0,subLn = postprocessorProperties.length; j < subLn; j++) {
6819 postprocessorProperty = postprocessorProperties[j];
6820
6821 if (data.hasOwnProperty(postprocessorProperty)) {
6822 postprocessors.push(postprocessor.fn);
6823 break;
6824 }
6825 }
6826 }
6827 }
6828 else {
6829 postprocessors.push(postprocessor);
6830 }
6831 }
6832
6833 process = function(clsName, cls, clsData) {
6834 postprocessor = postprocessors[index++];
6835
6836 if (!postprocessor) {
6837 Manager.set(className, cls);
6838
6839 if (createdFn) {
6840 createdFn.call(cls, cls);
6841 }
6842
6843 Manager.triggerCreated(className);
6844 return;
6845 }
6846
6847 if (postprocessor.call(this, clsName, cls, clsData, process) !== false) {
6848 process.apply(this, arguments);
6849 }
6850 };
6851
6852 process.call(Manager, className, this, data);
6853 });
6854 },
6855
6856 createOverride: function(className, data, createdFn) {
6857 var overriddenClassName = data.override,
6858 requires = Ext.Array.from(data.requires);
6859
6860 delete data.override;
6861 delete data.requires;
6862
6863 this.existCache[className] = true;
6864
6865 Ext.require(requires, function() {
6866 // Override the target class right after it's created
6867 this.onCreated(function() {
6868 var overridenClass = this.get(overriddenClassName);
6869 if (overridenClass.singleton) {
6870 overridenClass.self.override(data);
6871 }
6872 else {
6873 overridenClass.override(data);
6874 }
6875
6876 if (createdFn) {
6877 createdFn.call(overridenClass, overridenClass);
6878 }
6879
6880 // This push the overridding file itself into Ext.Loader.history
6881 // Hence if the target class never exists, the overriding file will
6882 // never be included in the build
6883 this.triggerCreated(className);
6884 }, this, overriddenClassName);
6885 }, this);
6886
6887 return this;
6888 },
6889
6890 /**
6891 * Instantiate a class by its alias; usually invoked by the convenient shorthand {@link Ext#createByAlias Ext.createByAlias}
6892 * If {@link Ext.Loader} is {@link Ext.Loader#setConfig enabled} and the class has not been defined yet, it will
6893 * attempt to load the class via synchronous loading.
6894 *
6895 * var window = Ext.ClassManager.instantiateByAlias('widget.window', { width: 600, height: 800 });
6896 *
6897 * @param {String} alias
6898 * @param {Mixed...} args Additional arguments after the alias will be passed to the class constructor.
6899 * @return {Object} instance
6900 */
6901 instantiateByAlias: function() {
6902 var alias = arguments[0],
6903 args = arraySlice.call(arguments),
6904 className = this.getNameByAlias(alias);
6905
6906 if (!className) {
6907 className = this.maps.aliasToName[alias];
6908
6909 //<debug error>
6910 if (!className) {
6911 throw new Error("[Ext.createByAlias] Cannot create an instance of unrecognized alias: " + alias);
6912 }
6913 //</debug>
6914
6915 //<debug warn>
6916 Ext.Logger.warn("[Ext.Loader] Synchronously loading '" + className + "'; consider adding " +
6917 "Ext.require('" + alias + "') above Ext.onReady");
6918 //</debug>
6919
6920 Ext.syncRequire(className);
6921 }
6922
6923 args[0] = className;
6924
6925 return this.instantiate.apply(this, args);
6926 },
6927
6928 /**
6929 * Instantiate a class by either full name, alias or alternate name; usually invoked by the convenient
6930 * shorthand {@link Ext.ClassManager#create Ext.create}.
6931 *
6932 * If {@link Ext.Loader} is {@link Ext.Loader#setConfig enabled} and the class has not been defined yet, it will
6933 * attempt to load the class via synchronous loading.
6934 *
6935 * For example, all these three lines return the same result:
6936 *
6937 * // alias
6938 * var formPanel = Ext.create('widget.formpanel', { width: 600, height: 800 });
6939 *
6940 * // alternate name
6941 * var formPanel = Ext.create('Ext.form.FormPanel', { width: 600, height: 800 });
6942 *
6943 * // full class name
6944 * var formPanel = Ext.create('Ext.form.Panel', { width: 600, height: 800 });
6945 *
6946 * @param {String} name
6947 * @param {Mixed} args Additional arguments after the name will be passed to the class' constructor.
6948 * @return {Object} instance
6949 */
6950 instantiate: function() {
6951 var name = arguments[0],
6952 args = arraySlice.call(arguments, 1),
6953 alias = name,
6954 possibleName, cls;
6955
6956 if (typeof name != 'function') {
6957 //<debug error>
6958 if ((typeof name != 'string' || name.length < 1)) {
6959 throw new Error("[Ext.create] Invalid class name or alias '" + name + "' specified, must be a non-empty string");
6960 }
6961 //</debug>
6962
6963 cls = this.get(name);
6964 }
6965 else {
6966 cls = name;
6967 }
6968
6969 // No record of this class name, it's possibly an alias, so look it up
6970 if (!cls) {
6971 possibleName = this.getNameByAlias(name);
6972
6973 if (possibleName) {
6974 name = possibleName;
6975
6976 cls = this.get(name);
6977 }
6978 }
6979
6980 // Still no record of this class name, it's possibly an alternate name, so look it up
6981 if (!cls) {
6982 possibleName = this.getNameByAlternate(name);
6983
6984 if (possibleName) {
6985 name = possibleName;
6986
6987 cls = this.get(name);
6988 }
6989 }
6990
6991 // Still not existing at this point, try to load it via synchronous mode as the last resort
6992 if (!cls) {
6993 //<debug warn>
6994 Ext.Logger.warn("[Ext.Loader] Synchronously loading '" + name + "'; consider adding '" +
6995 ((possibleName) ? alias : name) + "' explicitly as a require of the corresponding class");
6996 //</debug>
6997
6998 Ext.syncRequire(name);
6999
7000 cls = this.get(name);
7001 }
7002
7003 //<debug error>
7004 if (!cls) {
7005 throw new Error("[Ext.create] Cannot create an instance of unrecognized class name / alias: " + alias);
7006 }
7007
7008 if (typeof cls != 'function') {
7009 throw new Error("[Ext.create] '" + name + "' is a singleton and cannot be instantiated");
7010 }
7011 //</debug>
7012
7013 return this.getInstantiator(args.length)(cls, args);
7014 },
7015
7016 /**
7017 * @private
7018 * @param {String} name
7019 * @param {Array} args
7020 */
7021 dynInstantiate: function(name, args) {
7022 args = arrayFrom(args, true);
7023 args.unshift(name);
7024
7025 return this.instantiate.apply(this, args);
7026 },
7027
7028 /**
7029 * @private
7030 * @param {Number} length
7031 */
7032 getInstantiator: function(length) {
7033 var instantiators = this.instantiators,
7034 instantiator;
7035
7036 instantiator = instantiators[length];
7037
7038 if (!instantiator) {
7039 var i = length,
7040 args = [];
7041
7042 for (i = 0; i < length; i++) {
7043 args.push('a[' + i + ']');
7044 }
7045
7046 instantiator = instantiators[length] = new Function('c', 'a', 'return new c(' + args.join(',') + ')');
7047 //<debug>
7048 instantiator.displayName = "Ext.ClassManager.instantiate" + length;
7049 //</debug>
7050 }
7051
7052 return instantiator;
7053 },
7054
7055 /**
7056 * @private
7057 */
7058 postprocessors: {},
7059
7060 /**
7061 * @private
7062 */
7063 defaultPostprocessors: [],
7064
7065 /**
7066 * Register a post-processor function.
7067 * @private
7068 */
7069 registerPostprocessor: function(name, fn, properties, position, relativeTo) {
7070 if (!position) {
7071 position = 'last';
7072 }
7073
7074 if (!properties) {
7075 properties = [name];
7076 }
7077
7078 this.postprocessors[name] = {
7079 name: name,
7080 properties: properties || false,
7081 fn: fn
7082 };
7083
7084 this.setDefaultPostprocessorPosition(name, position, relativeTo);
7085
7086 return this;
7087 },
7088
7089 /**
7090 * Set the default post processors array stack which are applied to every class.
7091 *
7092 * @private
7093 * @param {String/Array} postprocessors The name of a registered post processor or an array of registered names.
7094 * @return {Ext.ClassManager} this
7095 */
7096 setDefaultPostprocessors: function(postprocessors) {
7097 this.defaultPostprocessors = arrayFrom(postprocessors);
7098
7099 return this;
7100 },
7101
7102 /**
7103 * Insert this post-processor at a specific position in the stack, optionally relative to
7104 * any existing post-processor
7105 *
7106 * @private
7107 * @param {String} name The post-processor name. Note that it needs to be registered with
7108 * {@link Ext.ClassManager#registerPostprocessor} before this
7109 * @param {String} offset The insertion position. Four possible values are:
7110 * 'first', 'last', or: 'before', 'after' (relative to the name provided in the third argument)
7111 * @param {String} relativeName
7112 * @return {Ext.ClassManager} this
7113 */
7114 setDefaultPostprocessorPosition: function(name, offset, relativeName) {
7115 var defaultPostprocessors = this.defaultPostprocessors,
7116 index;
7117
7118 if (typeof offset == 'string') {
7119 if (offset === 'first') {
7120 defaultPostprocessors.unshift(name);
7121
7122 return this;
7123 }
7124 else if (offset === 'last') {
7125 defaultPostprocessors.push(name);
7126
7127 return this;
7128 }
7129
7130 offset = (offset === 'after') ? 1 : -1;
7131 }
7132
7133 index = Ext.Array.indexOf(defaultPostprocessors, relativeName);
7134
7135 if (index !== -1) {
7136 Ext.Array.splice(defaultPostprocessors, Math.max(0, index + offset), 0, name);
7137 }
7138
7139 return this;
7140 },
7141
7142 /**
7143 * Converts a string expression to an array of matching class names. An expression can either refers to class aliases
7144 * or class names. Expressions support wildcards:
7145 *
7146 * // returns ['Ext.window.Window']
7147 * var window = Ext.ClassManager.getNamesByExpression('widget.window');
7148 *
7149 * // returns ['widget.panel', 'widget.window', ...]
7150 * var allWidgets = Ext.ClassManager.getNamesByExpression('widget.*');
7151 *
7152 * // returns ['Ext.data.Store', 'Ext.data.ArrayProxy', ...]
7153 * var allData = Ext.ClassManager.getNamesByExpression('Ext.data.*');
7154 *
7155 * @param {String} expression
7156 * @return {Array} classNames
7157 */
7158 getNamesByExpression: function(expression) {
7159 var nameToAliasesMap = this.maps.nameToAliases,
7160 names = [],
7161 name, alias, aliases, possibleName, regex, i, ln;
7162
7163 //<debug error>
7164 if (typeof expression != 'string' || expression.length < 1) {
7165 throw new Error("[Ext.ClassManager.getNamesByExpression] Expression " + expression + " is invalid, must be a non-empty string");
7166 }
7167 //</debug>
7168
7169 if (expression.indexOf('*') !== -1) {
7170 expression = expression.replace(/\*/g, '(.*?)');
7171 regex = new RegExp('^' + expression + '$');
7172
7173 for (name in nameToAliasesMap) {
7174 if (nameToAliasesMap.hasOwnProperty(name)) {
7175 aliases = nameToAliasesMap[name];
7176
7177 if (name.search(regex) !== -1) {
7178 names.push(name);
7179 }
7180 else {
7181 for (i = 0, ln = aliases.length; i < ln; i++) {
7182 alias = aliases[i];
7183
7184 if (alias.search(regex) !== -1) {
7185 names.push(name);
7186 break;
7187 }
7188 }
7189 }
7190 }
7191 }
7192
7193 } else {
7194 possibleName = this.getNameByAlias(expression);
7195
7196 if (possibleName) {
7197 names.push(possibleName);
7198 } else {
7199 possibleName = this.getNameByAlternate(expression);
7200
7201 if (possibleName) {
7202 names.push(possibleName);
7203 } else {
7204 names.push(expression);
7205 }
7206 }
7207 }
7208
7209 return names;
7210 }
7211 };
7212
7213 //<feature classSystem.alias>
7214 /**
7215 * @cfg {String[]} alias
7216 * @member Ext.Class
7217 * List of short aliases for class names. Most useful for defining xtypes for widgets:
7218 *
7219 * Ext.define('MyApp.CoolPanel', {
7220 * extend: 'Ext.panel.Panel',
7221 * alias: ['widget.coolpanel'],
7222 *
7223 * config: {
7224 * html : 'Yeah!'
7225 * }
7226 * });
7227 *
7228 * // Using Ext.create
7229 * Ext.create('widget.coolpanel');
7230 *
7231 * // Using the shorthand for widgets and in xtypes
7232 * Ext.widget('panel', {
7233 * items: [
7234 * {xtype: 'coolpanel', html: 'Foo'},
7235 * {xtype: 'coolpanel', html: 'Bar'}
7236 * ]
7237 * });
7238 *
7239 * For {@link Ext.Component}, you can also use the {@link Ext.Component#xtype} property.
7240 */
7241 /**
7242 * @cfg {String[]} xtype
7243 * @member Ext.Component
7244 * List of xtypes for {@link Ext.Component}. XTypes must not contain periods.
7245 *
7246 * Ext.define('MyApp.CoolPanel', {
7247 * extend: 'Ext.panel.Panel',
7248 * xtype: 'coolpanel',
7249 *
7250 * config: {
7251 * html : 'Yeah!'
7252 * }
7253 * });
7254 *
7255 * // Using Ext.create
7256 * Ext.create('widget.coolpanel');
7257 *
7258 * // Using the shorthand for widgets and in xtypes
7259 * Ext.widget('panel', {
7260 * items: [
7261 * {xtype: 'coolpanel', html: 'Foo'},
7262 * {xtype: 'coolpanel', html: 'Bar'}
7263 * ]
7264 * });
7265 */
7266 Manager.registerPostprocessor('alias', function(name, cls, data) {
7267 var aliases = data.alias,
7268 i, ln;
7269
7270 for (i = 0,ln = aliases.length; i < ln; i++) {
7271 alias = aliases[i];
7272
7273 this.setAlias(cls, alias);
7274 }
7275
7276 }, ['xtype', 'alias']);
7277 //</feature>
7278
7279 //<feature classSystem.singleton>
7280 /**
7281 * @cfg {Boolean} singleton
7282 * @member Ext.Class
7283 * When set to true, the class will be instantiated as singleton. For example:
7284 *
7285 * Ext.define('Logger', {
7286 * singleton: true,
7287 * log: function(msg) {
7288 * console.log(msg);
7289 * }
7290 * });
7291 *
7292 * Logger.log('Hello');
7293 */
7294 Manager.registerPostprocessor('singleton', function(name, cls, data, fn) {
7295 fn.call(this, name, new cls(), data);
7296 return false;
7297 });
7298 //</feature>
7299
7300 //<feature classSystem.alternateClassName>
7301 /**
7302 * @cfg {String/String[]} alternateClassName
7303 * @member Ext.Class
7304 * Defines alternate names for this class. For example:
7305 *
7306 * @example
7307 * Ext.define('Developer', {
7308 * alternateClassName: ['Coder', 'Hacker'],
7309 * code: function(msg) {
7310 * alert('Typing... ' + msg);
7311 * }
7312 * });
7313 *
7314 * var joe = Ext.create('Developer');
7315 * joe.code('stackoverflow');
7316 *
7317 * var rms = Ext.create('Hacker');
7318 * rms.code('hack hack');
7319 */
7320 Manager.registerPostprocessor('alternateClassName', function(name, cls, data) {
7321 var alternates = data.alternateClassName,
7322 i, ln, alternate;
7323
7324 if (!(alternates instanceof Array)) {
7325 alternates = [alternates];
7326 }
7327
7328 for (i = 0, ln = alternates.length; i < ln; i++) {
7329 alternate = alternates[i];
7330
7331 //<debug error>
7332 if (typeof alternate != 'string') {
7333 throw new Error("[Ext.define] Invalid alternate of: '" + alternate + "' for class: '" + name + "'; must be a valid string");
7334 }
7335 //</debug>
7336
7337 this.set(alternate, cls);
7338 }
7339 });
7340 //</feature>
7341
7342 Ext.apply(Ext, {
7343 /**
7344 * Instantiate a class by either full name, alias or alternate name.
7345 *
7346 * If {@link Ext.Loader} is {@link Ext.Loader#setConfig enabled} and the class has not been defined yet, it will
7347 * attempt to load the class via synchronous loading.
7348 *
7349 * For example, all these three lines return the same result:
7350 *
7351 * // alias
7352 * var formPanel = Ext.create('widget.formpanel', { width: 600, height: 800 });
7353 *
7354 * // alternate name
7355 * var formPanel = Ext.create('Ext.form.FormPanel', { width: 600, height: 800 });
7356 *
7357 * // full class name
7358 * var formPanel = Ext.create('Ext.form.Panel', { width: 600, height: 800 });
7359 *
7360 * @param {String} name
7361 * @param {Mixed} args Additional arguments after the name will be passed to the class' constructor.
7362 * @return {Object} instance
7363 * @member Ext
7364 */
7365 create: alias(Manager, 'instantiate'),
7366
7367 /**
7368 * Convenient shorthand to create a widget by its xtype, also see {@link Ext.ClassManager#instantiateByAlias}
7369 *
7370 * var button = Ext.widget('button'); // Equivalent to Ext.create('widget.button')
7371 * var panel = Ext.widget('panel'); // Equivalent to Ext.create('widget.panel')
7372 *
7373 * @member Ext
7374 * @method widget
7375 * @param {String} name
7376 * @return {Object} instance
7377 */
7378 widget: function(name) {
7379 var args = arraySlice.call(arguments);
7380 args[0] = 'widget.' + name;
7381
7382 return Manager.instantiateByAlias.apply(Manager, args);
7383 },
7384
7385 /**
7386 * Convenient shorthand, see {@link Ext.ClassManager#instantiateByAlias}.
7387 * @member Ext
7388 * @method createByAlias
7389 * @param {String} alias
7390 * @param {Mixed...} args Additional arguments after the alias will be passed to the class constructor.
7391 * @return {Object} instance
7392 */
7393 createByAlias: alias(Manager, 'instantiateByAlias'),
7394
7395 /**
7396 * Defines a class or override. A basic class is defined like this:
7397 *
7398 * Ext.define('My.awesome.Class', {
7399 * someProperty: 'something',
7400 *
7401 * someMethod: function(s) {
7402 * console.log(s + this.someProperty);
7403 * }
7404 * });
7405 *
7406 * var obj = new My.awesome.Class();
7407 *
7408 * obj.someMethod('Say '); // logs 'Say something' to the console
7409 *
7410 * To defines an override, include the `override` property. The content of an
7411 * override is aggregated with the specified class in order to extend or modify
7412 * that class. This can be as simple as setting default property values or it can
7413 * extend and/or replace methods. This can also extend the statics of the class.
7414 *
7415 * One use for an override is to break a large class into manageable pieces.
7416 *
7417 * // File: /src/app/Panel.js
7418 * Ext.define('My.app.Panel', {
7419 * extend: 'Ext.panel.Panel',
7420 * requires: [
7421 * 'My.app.PanelPart2',
7422 * 'My.app.PanelPart3'
7423 * ],
7424 *
7425 * constructor: function (config) {
7426 * this.callParent(arguments); // calls Ext.panel.Panel's constructor
7427 * // ...
7428 * },
7429 *
7430 * statics: {
7431 * method: function () {
7432 * return 'abc';
7433 * }
7434 * }
7435 * });
7436 *
7437 * // File: /src/app/PanelPart2.js
7438 * Ext.define('My.app.PanelPart2', {
7439 * override: 'My.app.Panel',
7440 *
7441 * constructor: function (config) {
7442 * this.callParent(arguments); // calls My.app.Panel's constructor
7443 * // ...
7444 * }
7445 * });
7446 *
7447 * Another use for an override is to provide optional parts of classes that can be
7448 * independently required. In this case, the class may even be unaware of the
7449 * override altogether.
7450 *
7451 * Ext.define('My.ux.CoolTip', {
7452 * override: 'Ext.tip.ToolTip',
7453 *
7454 * constructor: function (config) {
7455 * this.callParent(arguments); // calls Ext.tip.ToolTip's constructor
7456 * // ...
7457 * }
7458 * });
7459 *
7460 * The above override can now be required as normal.
7461 *
7462 * Ext.define('My.app.App', {
7463 * requires: [
7464 * 'My.ux.CoolTip'
7465 * ]
7466 * });
7467 *
7468 * Overrides can also contain statics:
7469 *
7470 * Ext.define('My.app.BarMod', {
7471 * override: 'Ext.foo.Bar',
7472 *
7473 * statics: {
7474 * method: function (x) {
7475 * return this.callParent([x * 2]); // call Ext.foo.Bar.method
7476 * }
7477 * }
7478 * });
7479 *
7480 * __IMPORTANT:__ An override is only included in a build if the class it overrides is
7481 * required. Otherwise, the override, like the target class, is not included.
7482 *
7483 * @param {String} className The class name to create in string dot-namespaced format, for example:
7484 * 'My.very.awesome.Class', 'FeedViewer.plugin.CoolPager'
7485 *
7486 * It is highly recommended to follow this simple convention:
7487 * - The root and the class name are 'CamelCased'
7488 * - Everything else is lower-cased
7489 *
7490 * @param {Object} data The key - value pairs of properties to apply to this class. Property names can be of
7491 * any valid strings, except those in the reserved listed below:
7492 *
7493 * - `mixins`
7494 * - `statics`
7495 * - `config`
7496 * - `alias`
7497 * - `xtype` (for {@link Ext.Component}s only)
7498 * - `self`
7499 * - `singleton`
7500 * - `alternateClassName`
7501 * - `override`
7502 *
7503 * @param {Function} [createdFn] Optional callback to execute after the class (or override)
7504 * is created. The execution scope (`this`) will be the newly created class itself.
7505 * @return {Ext.Base}
7506 *
7507 * @member Ext
7508 * @method define
7509 */
7510 define: function (className, data, createdFn) {
7511 if ('override' in data) {
7512 return Manager.createOverride.apply(Manager, arguments);
7513 }
7514
7515 return Manager.create.apply(Manager, arguments);
7516 },
7517
7518 /**
7519 * Convenient shorthand for {@link Ext.ClassManager#getName}.
7520 * @member Ext
7521 * @method getClassName
7522 * @inheritdoc Ext.ClassManager#getName
7523 */
7524 getClassName: alias(Manager, 'getName'),
7525
7526 /**
7527 * Returns the display name for object. This name is looked for in order from the following places:
7528 *
7529 * - `displayName` field of the object.
7530 * - `$name` and `$class` fields of the object.
7531 * - '$className` field of the object.
7532 *
7533 * This method is used by {@link Ext.Logger#log} to display information about objects.
7534 *
7535 * @param {Mixed} [object] The object who's display name to determine.
7536 * @return {String} The determined display name, or "Anonymous" if none found.
7537 * @member Ext
7538 */
7539 getDisplayName: function(object) {
7540 if (object) {
7541 if (object.displayName) {
7542 return object.displayName;
7543 }
7544
7545 if (object.$name && object.$class) {
7546 return Ext.getClassName(object.$class) + '#' + object.$name;
7547 }
7548
7549 if (object.$className) {
7550 return object.$className;
7551 }
7552 }
7553
7554 return 'Anonymous';
7555 },
7556
7557 /**
7558 * Convenient shorthand, see {@link Ext.ClassManager#getClass}.
7559 * @member Ext
7560 * @method getClass
7561 */
7562 getClass: alias(Manager, 'getClass'),
7563
7564 /**
7565 * Creates namespaces to be used for scoping variables and classes so that they are not global.
7566 * Specifying the last node of a namespace implicitly creates all other nodes. Usage:
7567 *
7568 * Ext.namespace('Company', 'Company.data');
7569 *
7570 * // equivalent and preferable to the above syntax
7571 * Ext.namespace('Company.data');
7572 *
7573 * Company.Widget = function() {
7574 * // ...
7575 * };
7576 *
7577 * Company.data.CustomStore = function(config) {
7578 * // ...
7579 * };
7580 *
7581 * @param {String} namespace1
7582 * @param {String} namespace2
7583 * @param {String} etc
7584 * @return {Object} The namespace object. If multiple arguments are passed, this will be the last namespace created.
7585 * @member Ext
7586 * @method namespace
7587 */
7588 namespace: alias(Manager, 'createNamespaces')
7589 });
7590
7591 /**
7592 * Old name for {@link Ext#widget}.
7593 * @deprecated 2.0.0 Please use {@link Ext#widget} instead.
7594 * @method createWidget
7595 * @member Ext
7596 */
7597 Ext.createWidget = Ext.widget;
7598
7599 /**
7600 * Convenient alias for {@link Ext#namespace Ext.namespace}.
7601 * @member Ext
7602 * @method ns
7603 */
7604 Ext.ns = Ext.namespace;
7605
7606 Class.registerPreprocessor('className', function(cls, data) {
7607 if (data.$className) {
7608 cls.$className = data.$className;
7609 //<debug>
7610 cls.displayName = cls.$className;
7611 //</debug>
7612 }
7613 }, true, 'first');
7614
7615 Class.registerPreprocessor('alias', function(cls, data) {
7616 var prototype = cls.prototype,
7617 xtypes = arrayFrom(data.xtype),
7618 aliases = arrayFrom(data.alias),
7619 widgetPrefix = 'widget.',
7620 widgetPrefixLength = widgetPrefix.length,
7621 xtypesChain = Array.prototype.slice.call(prototype.xtypesChain || []),
7622 xtypesMap = Ext.merge({}, prototype.xtypesMap || {}),
7623 i, ln, alias, xtype;
7624
7625 for (i = 0,ln = aliases.length; i < ln; i++) {
7626 alias = aliases[i];
7627
7628 //<debug error>
7629 if (typeof alias != 'string' || alias.length < 1) {
7630 throw new Error("[Ext.define] Invalid alias of: '" + alias + "' for class: '" + name + "'; must be a valid string");
7631 }
7632 //</debug>
7633
7634 if (alias.substring(0, widgetPrefixLength) === widgetPrefix) {
7635 xtype = alias.substring(widgetPrefixLength);
7636 Ext.Array.include(xtypes, xtype);
7637 }
7638 }
7639
7640 cls.xtype = data.xtype = xtypes[0];
7641 data.xtypes = xtypes;
7642
7643 for (i = 0,ln = xtypes.length; i < ln; i++) {
7644 xtype = xtypes[i];
7645
7646 if (!xtypesMap[xtype]) {
7647 xtypesMap[xtype] = true;
7648 xtypesChain.push(xtype);
7649 }
7650 }
7651
7652 data.xtypesChain = xtypesChain;
7653 data.xtypesMap = xtypesMap;
7654
7655 Ext.Function.interceptAfter(data, 'onClassCreated', function() {
7656 var mixins = prototype.mixins,
7657 key, mixin;
7658
7659 for (key in mixins) {
7660 if (mixins.hasOwnProperty(key)) {
7661 mixin = mixins[key];
7662
7663 xtypes = mixin.xtypes;
7664
7665 if (xtypes) {
7666 for (i = 0,ln = xtypes.length; i < ln; i++) {
7667 xtype = xtypes[i];
7668
7669 if (!xtypesMap[xtype]) {
7670 xtypesMap[xtype] = true;
7671 xtypesChain.push(xtype);
7672 }
7673 }
7674 }
7675 }
7676 }
7677 });
7678
7679 for (i = 0,ln = xtypes.length; i < ln; i++) {
7680 xtype = xtypes[i];
7681
7682 //<debug error>
7683 if (typeof xtype != 'string' || xtype.length < 1) {
7684 throw new Error("[Ext.define] Invalid xtype of: '" + xtype + "' for class: '" + name + "'; must be a valid non-empty string");
7685 }
7686 //</debug>
7687
7688 Ext.Array.include(aliases, widgetPrefix + xtype);
7689 }
7690
7691 data.alias = aliases;
7692
7693 }, ['xtype', 'alias']);
7694
7695 })(Ext.Class, Ext.Function.alias, Array.prototype.slice, Ext.Array.from, Ext.global);
7696
7697 //@tag foundation,core
7698 //@define Ext.Loader
7699 //@require Ext.ClassManager
7700
7701 /**
7702 * @class Ext.Loader
7703 *
7704 * @author Jacky Nguyen <jacky@sencha.com>
7705 * @docauthor Jacky Nguyen <jacky@sencha.com>
7706 *
7707 * Ext.Loader is the heart of the new dynamic dependency loading capability in Ext JS 4+. It is most commonly used
7708 * via the {@link Ext#require} shorthand. Ext.Loader supports both asynchronous and synchronous loading
7709 * approaches, and leverage their advantages for the best development flow.
7710 * We'll discuss about the pros and cons of each approach.
7711 *
7712 * __Note:__ The Loader is only enabled by default in development versions of the library (eg sencha-touch-debug.js). To
7713 * explicitly enable the loader, use `Ext.Loader.setConfig({ enabled: true });` before the start of your script.
7714 *
7715 * ## Asynchronous Loading
7716 *
7717 * - Advantages:
7718 * + Cross-domain
7719 * + No web server needed: you can run the application via the file system protocol (i.e: `file://path/to/your/index
7720 * .html`)
7721 * + Best possible debugging experience: error messages come with the exact file name and line number
7722 *
7723 * - Disadvantages:
7724 * + Dependencies need to be specified before-hand
7725 *
7726 * ### Method 1: Explicitly include what you need: ###
7727 *
7728 * // Syntax
7729 * // Ext.require({String/Array} expressions);
7730 *
7731 * // Example: Single alias
7732 * Ext.require('widget.window');
7733 *
7734 * // Example: Single class name
7735 * Ext.require('Ext.window.Window');
7736 *
7737 * // Example: Multiple aliases / class names mix
7738 * Ext.require(['widget.window', 'layout.border', 'Ext.data.Connection']);
7739 *
7740 * // Wildcards
7741 * Ext.require(['widget.*', 'layout.*', 'Ext.data.*']);
7742 *
7743 * ### Method 2: Explicitly exclude what you don't need: ###
7744 *
7745 * // Syntax: Note that it must be in this chaining format.
7746 * // Ext.exclude({String/Array} expressions)
7747 * // .require({String/Array} expressions);
7748 *
7749 * // Include everything except Ext.data.*
7750 * Ext.exclude('Ext.data.*').require('*');
7751 *
7752 * // Include all widgets except widget.checkbox*,
7753 * // which will match widget.checkbox, widget.checkboxfield, widget.checkboxgroup, etc.
7754 * Ext.exclude('widget.checkbox*').require('widget.*');
7755 *
7756 * # Synchronous Loading on Demand #
7757 *
7758 * - *Advantages:*
7759 * + There's no need to specify dependencies before-hand, which is always the convenience of including ext-all.js
7760 * before
7761 *
7762 * - *Disadvantages:*
7763 * + Not as good debugging experience since file name won't be shown (except in Firebug at the moment)
7764 * + Must be from the same domain due to XHR restriction
7765 * + Need a web server, same reason as above
7766 *
7767 * There's one simple rule to follow: Instantiate everything with Ext.create instead of the `new` keyword
7768 *
7769 * Ext.create('widget.window', {}); // Instead of new Ext.window.Window({...});
7770 *
7771 * Ext.create('Ext.window.Window', {}); // Same as above, using full class name instead of alias
7772 *
7773 * Ext.widget('window', {}); // Same as above, all you need is the traditional `xtype`
7774 *
7775 * Behind the scene, {@link Ext.ClassManager} will automatically check whether the given class name / alias has already
7776 * existed on the page. If it's not, Ext.Loader will immediately switch itself to synchronous mode and automatic load the given
7777 * class and all its dependencies.
7778 *
7779 * # Hybrid Loading - The Best of Both Worlds #
7780 *
7781 * It has all the advantages combined from asynchronous and synchronous loading. The development flow is simple:
7782 *
7783 * ### Step 1: Start writing your application using synchronous approach. ###
7784 * Ext.Loader will automatically fetch all dependencies on demand as they're
7785 * needed during run-time. For example:
7786 *
7787 * Ext.onReady(function(){
7788 * var window = Ext.createWidget('window', {
7789 * width: 500,
7790 * height: 300,
7791 * layout: {
7792 * type: 'border',
7793 * padding: 5
7794 * },
7795 * title: 'Hello Dialog',
7796 * items: [{
7797 * title: 'Navigation',
7798 * collapsible: true,
7799 * region: 'west',
7800 * width: 200,
7801 * html: 'Hello',
7802 * split: true
7803 * }, {
7804 * title: 'TabPanel',
7805 * region: 'center'
7806 * }]
7807 * });
7808 *
7809 * window.show();
7810 * });
7811 *
7812 * ### Step 2: Along the way, when you need better debugging ability, watch the console for warnings like these: ###
7813 *
7814 * [Ext.Loader] Synchronously loading 'Ext.window.Window'; consider adding Ext.require('Ext.window.Window') before your application's code
7815 * ClassManager.js:432
7816 * [Ext.Loader] Synchronously loading 'Ext.layout.container.Border'; consider adding Ext.require('Ext.layout.container.Border') before your application's code
7817 *
7818 * Simply copy and paste the suggested code above `Ext.onReady`, i.e:
7819 *
7820 * Ext.require('Ext.window.Window');
7821 * Ext.require('Ext.layout.container.Border');
7822 *
7823 * Ext.onReady(function () {
7824 * // ...
7825 * });
7826 *
7827 * Everything should now load via asynchronous mode.
7828 *
7829 * # Deployment #
7830 *
7831 * It's important to note that dynamic loading should only be used during development on your local machines.
7832 * During production, all dependencies should be combined into one single JavaScript file. Ext.Loader makes
7833 * the whole process of transitioning from / to between development / maintenance and production as easy as
7834 * possible. Internally {@link Ext.Loader#history Ext.Loader.history} maintains the list of all dependencies your application
7835 * needs in the exact loading sequence. It's as simple as concatenating all files in this array into one,
7836 * then include it on top of your application.
7837 *
7838 * @singleton
7839 */
7840 (function(Manager, Class, flexSetter, alias, pass, arrayFrom, arrayErase, arrayInclude) {
7841
7842 var
7843 dependencyProperties = ['extend', 'mixins', 'requires'],
7844 Loader,
7845 setPathCount = 0;;
7846
7847 Loader = Ext.Loader = {
7848
7849 /**
7850 * @private
7851 */
7852 isInHistory: {},
7853
7854 /**
7855 * An array of class names to keep track of the dependency loading order.
7856 * This is not guaranteed to be the same every time due to the asynchronous
7857 * nature of the Loader.
7858 *
7859 * @property history
7860 * @type Array
7861 */
7862 history: [],
7863
7864 /**
7865 * Configuration
7866 * @private
7867 */
7868 config: {
7869 /**
7870 * Whether or not to enable the dynamic dependency loading feature.
7871 * @cfg {Boolean} enabled
7872 */
7873 enabled: true,
7874
7875 /**
7876 * @cfg {Boolean} disableCaching
7877 * Appends current timestamp to script files to prevent caching.
7878 */
7879 disableCaching: true,
7880
7881 /**
7882 * @cfg {String} disableCachingParam
7883 * The get parameter name for the cache buster's timestamp.
7884 */
7885 disableCachingParam: '_dc',
7886
7887 /**
7888 * @cfg {Object} paths
7889 * The mapping from namespaces to file paths.
7890 *
7891 * {
7892 * 'Ext': '.', // This is set by default, Ext.layout.container.Container will be
7893 * // loaded from ./layout/Container.js
7894 *
7895 * 'My': './src/my_own_folder' // My.layout.Container will be loaded from
7896 * // ./src/my_own_folder/layout/Container.js
7897 * }
7898 *
7899 * Note that all relative paths are relative to the current HTML document.
7900 * If not being specified, for example, `Other.awesome.Class`
7901 * will simply be loaded from `./Other/awesome/Class.js`.
7902 */
7903 paths: {
7904 'Ext': '.'
7905 }
7906 },
7907
7908 /**
7909 * Set the configuration for the loader. This should be called right after ext-(debug).js
7910 * is included in the page, and before Ext.onReady. i.e:
7911 *
7912 * <script type="text/javascript" src="ext-core-debug.js"></script>
7913 * <script type="text/javascript">
7914 * Ext.Loader.setConfig({
7915 * enabled: true,
7916 * paths: {
7917 * 'My': 'my_own_path'
7918 * }
7919 * });
7920 * <script>
7921 * <script type="text/javascript">
7922 * Ext.require(...);
7923 *
7924 * Ext.onReady(function() {
7925 * // application code here
7926 * });
7927 * </script>
7928 *
7929 * Refer to config options of {@link Ext.Loader} for the list of possible properties.
7930 *
7931 * @param {Object/String} name The config object to override the default values
7932 * or name of a single config setting when also passing the second parameter.
7933 * @param {Mixed} [value] The value for the config setting.
7934 * @return {Ext.Loader} this
7935 */
7936 setConfig: function(name, value) {
7937 if (Ext.isObject(name) && arguments.length === 1) {
7938 Ext.merge(this.config, name);
7939 }
7940 else {
7941 this.config[name] = (Ext.isObject(value)) ? Ext.merge(this.config[name], value) : value;
7942 }
7943 setPathCount += 1;
7944 return this;
7945 },
7946
7947 /**
7948 * Get the config value corresponding to the specified name. If no name is given, will return the config object.
7949 * @param {String} name The config property name.
7950 * @return {Object/Mixed}
7951 */
7952 getConfig: function(name) {
7953 if (name) {
7954 return this.config[name];
7955 }
7956
7957 return this.config;
7958 },
7959
7960 /**
7961 * Sets the path of a namespace.
7962 * For example:
7963 *
7964 * Ext.Loader.setPath('Ext', '.');
7965 *
7966 * @param {String/Object} name See {@link Ext.Function#flexSetter flexSetter}
7967 * @param {String} [path] See {@link Ext.Function#flexSetter flexSetter}
7968 * @return {Ext.Loader} this
7969 * @method
7970 */
7971 setPath: flexSetter(function(name, path) {
7972 this.config.paths[name] = path;
7973 setPathCount += 1;
7974 return this;
7975 }),
7976
7977 /**
7978 * Sets a batch of path entries
7979 *
7980 * @param {Object } paths a set of className: path mappings
7981 * @return {Ext.Loader} this
7982 */
7983 addClassPathMappings: function(paths) {
7984 var name;
7985
7986 if(setPathCount == 0){
7987 Loader.config.paths = paths;
7988 } else {
7989 for(name in paths){
7990 Loader.config.paths[name] = paths[name];
7991 }
7992 }
7993 setPathCount++;
7994 return Loader;
7995 },
7996
7997 /**
7998 * Translates a className to a file path by adding the
7999 * the proper prefix and converting the .'s to /'s. For example:
8000 *
8001 * Ext.Loader.setPath('My', '/path/to/My');
8002 *
8003 * alert(Ext.Loader.getPath('My.awesome.Class')); // alerts '/path/to/My/awesome/Class.js'
8004 *
8005 * Note that the deeper namespace levels, if explicitly set, are always resolved first. For example:
8006 *
8007 * Ext.Loader.setPath({
8008 * 'My': '/path/to/lib',
8009 * 'My.awesome': '/other/path/for/awesome/stuff',
8010 * 'My.awesome.more': '/more/awesome/path'
8011 * });
8012 *
8013 * alert(Ext.Loader.getPath('My.awesome.Class')); // alerts '/other/path/for/awesome/stuff/Class.js'
8014 *
8015 * alert(Ext.Loader.getPath('My.awesome.more.Class')); // alerts '/more/awesome/path/Class.js'
8016 *
8017 * alert(Ext.Loader.getPath('My.cool.Class')); // alerts '/path/to/lib/cool/Class.js'
8018 *
8019 * alert(Ext.Loader.getPath('Unknown.strange.Stuff')); // alerts 'Unknown/strange/Stuff.js'
8020 *
8021 * @param {String} className
8022 * @return {String} path
8023 */
8024 getPath: function(className) {
8025 var path = '',
8026 paths = this.config.paths,
8027 prefix = this.getPrefix(className);
8028
8029 if (prefix.length > 0) {
8030 if (prefix === className) {
8031 return paths[prefix];
8032 }
8033
8034 path = paths[prefix];
8035 className = className.substring(prefix.length + 1);
8036 }
8037
8038 if (path.length > 0) {
8039 path += '/';
8040 }
8041
8042 return path.replace(/\/\.\//g, '/') + className.replace(/\./g, "/") + '.js';
8043 },
8044
8045 /**
8046 * @private
8047 * @param {String} className
8048 */
8049 getPrefix: function(className) {
8050 var paths = this.config.paths,
8051 prefix, deepestPrefix = '';
8052
8053 if (paths.hasOwnProperty(className)) {
8054 return className;
8055 }
8056
8057 for (prefix in paths) {
8058 if (paths.hasOwnProperty(prefix) && prefix + '.' === className.substring(0, prefix.length + 1)) {
8059 if (prefix.length > deepestPrefix.length) {
8060 deepestPrefix = prefix;
8061 }
8062 }
8063 }
8064
8065 return deepestPrefix;
8066 },
8067
8068 /**
8069 * Loads all classes by the given names and all their direct dependencies; optionally executes the given callback function when
8070 * finishes, within the optional scope. This method is aliased by {@link Ext#require Ext.require} for convenience.
8071 * @param {String/Array} expressions Can either be a string or an array of string.
8072 * @param {Function} fn (optional) The callback function.
8073 * @param {Object} scope (optional) The execution scope (`this`) of the callback function.
8074 * @param {String/Array} excludes (optional) Classes to be excluded, useful when being used with expressions.
8075 */
8076 require: function(expressions, fn, scope, excludes) {
8077 if (fn) {
8078 fn.call(scope);
8079 }
8080 },
8081
8082 /**
8083 * Synchronously loads all classes by the given names and all their direct dependencies; optionally executes the given callback function when finishes, within the optional scope. This method is aliased by {@link Ext#syncRequire} for convenience
8084 * @param {String/Array} expressions Can either be a string or an array of string
8085 * @param {Function} fn (optional) The callback function
8086 * @param {Object} scope (optional) The execution scope (`this`) of the callback function
8087 * @param {String/Array} excludes (optional) Classes to be excluded, useful when being used with expressions
8088 */
8089 syncRequire: function() {},
8090
8091 /**
8092 * Explicitly exclude files from being loaded. Useful when used in conjunction with a broad include expression.
8093 * Can be chained with more `require` and `exclude` methods, eg:
8094 *
8095 * Ext.exclude('Ext.data.*').require('*');
8096 *
8097 * Ext.exclude('widget.button*').require('widget.*');
8098 *
8099 * @param {Array} excludes
8100 * @return {Object} object contains `require` method for chaining.
8101 */
8102 exclude: function(excludes) {
8103 var me = this;
8104
8105 return {
8106 require: function(expressions, fn, scope) {
8107 return me.require(expressions, fn, scope, excludes);
8108 },
8109
8110 syncRequire: function(expressions, fn, scope) {
8111 return me.syncRequire(expressions, fn, scope, excludes);
8112 }
8113 };
8114 },
8115
8116 /**
8117 * Add a new listener to be executed when all required scripts are fully loaded.
8118 *
8119 * @param {Function} fn The function callback to be executed.
8120 * @param {Object} scope The execution scope (`this`) of the callback function.
8121 * @param {Boolean} withDomReady Whether or not to wait for document DOM ready as well.
8122 */
8123 onReady: function(fn, scope, withDomReady, options) {
8124 var oldFn;
8125
8126 if (withDomReady !== false && Ext.onDocumentReady) {
8127 oldFn = fn;
8128
8129 fn = function() {
8130 Ext.onDocumentReady(oldFn, scope, options);
8131 };
8132 }
8133
8134 fn.call(scope);
8135 }
8136 };
8137
8138 //<feature classSystem.loader>
8139 Ext.apply(Loader, {
8140 /**
8141 * @private
8142 */
8143 documentHead: typeof document != 'undefined' && (document.head || document.getElementsByTagName('head')[0]),
8144
8145 /**
8146 * Flag indicating whether there are still files being loaded
8147 * @private
8148 */
8149 isLoading: false,
8150
8151 /**
8152 * Maintain the queue for all dependencies. Each item in the array is an object of the format:
8153 *
8154 * {
8155 * requires: [...], // The required classes for this queue item
8156 * callback: function() { ... } // The function to execute when all classes specified in requires exist
8157 * }
8158 * @private
8159 */
8160 queue: [],
8161
8162 /**
8163 * Maintain the list of files that have already been handled so that they never get double-loaded
8164 * @private
8165 */
8166 isClassFileLoaded: {},
8167
8168 /**
8169 * @private
8170 */
8171 isFileLoaded: {},
8172
8173 /**
8174 * Maintain the list of listeners to execute when all required scripts are fully loaded
8175 * @private
8176 */
8177 readyListeners: [],
8178
8179 /**
8180 * Contains optional dependencies to be loaded last
8181 * @private
8182 */
8183 optionalRequires: [],
8184
8185 /**
8186 * Map of fully qualified class names to an array of dependent classes.
8187 * @private
8188 */
8189 requiresMap: {},
8190
8191 /**
8192 * @private
8193 */
8194 numPendingFiles: 0,
8195
8196 /**
8197 * @private
8198 */
8199 numLoadedFiles: 0,
8200
8201 /** @private */
8202 hasFileLoadError: false,
8203
8204 /**
8205 * @private
8206 */
8207 classNameToFilePathMap: {},
8208
8209 /**
8210 * @private
8211 */
8212 syncModeEnabled: false,
8213
8214 scriptElements: {},
8215
8216 /**
8217 * Refresh all items in the queue. If all dependencies for an item exist during looping,
8218 * it will execute the callback and call refreshQueue again. Triggers onReady when the queue is
8219 * empty
8220 * @private
8221 */
8222 refreshQueue: function() {
8223 var queue = this.queue,
8224 ln = queue.length,
8225 i, item, j, requires, references;
8226
8227 if (ln === 0) {
8228 this.triggerReady();
8229 return;
8230 }
8231
8232 for (i = 0; i < ln; i++) {
8233 item = queue[i];
8234
8235 if (item) {
8236 requires = item.requires;
8237 references = item.references;
8238
8239 // Don't bother checking when the number of files loaded
8240 // is still less than the array length
8241 if (requires.length > this.numLoadedFiles) {
8242 continue;
8243 }
8244
8245 j = 0;
8246
8247 do {
8248 if (Manager.isCreated(requires[j])) {
8249 // Take out from the queue
8250 arrayErase(requires, j, 1);
8251 }
8252 else {
8253 j++;
8254 }
8255 } while (j < requires.length);
8256
8257 if (item.requires.length === 0) {
8258 arrayErase(queue, i, 1);
8259 item.callback.call(item.scope);
8260 this.refreshQueue();
8261 break;
8262 }
8263 }
8264 }
8265
8266 return this;
8267 },
8268
8269 /**
8270 * Inject a script element to document's head, call onLoad and onError accordingly
8271 * @private
8272 */
8273 injectScriptElement: function(url, onLoad, onError, scope, charset) {
8274 var script = document.createElement('script'),
8275 me = this,
8276 onLoadFn = function() {
8277 me.cleanupScriptElement(script);
8278 onLoad.call(scope);
8279 },
8280 onErrorFn = function() {
8281 me.cleanupScriptElement(script);
8282 onError.call(scope);
8283 };
8284
8285 script.type = 'text/javascript';
8286 script.src = url;
8287 script.onload = onLoadFn;
8288 script.onerror = onErrorFn;
8289 script.onreadystatechange = function() {
8290 if (this.readyState === 'loaded' || this.readyState === 'complete') {
8291 onLoadFn();
8292 }
8293 };
8294
8295 if (charset) {
8296 script.charset = charset;
8297 }
8298
8299 this.documentHead.appendChild(script);
8300
8301 return script;
8302 },
8303
8304 removeScriptElement: function(url) {
8305 var scriptElements = this.scriptElements;
8306
8307 if (scriptElements[url]) {
8308 this.cleanupScriptElement(scriptElements[url], true);
8309 delete scriptElements[url];
8310 }
8311
8312 return this;
8313 },
8314
8315 /**
8316 * @private
8317 */
8318 cleanupScriptElement: function(script, remove) {
8319 script.onload = null;
8320 script.onreadystatechange = null;
8321 script.onerror = null;
8322
8323 if (remove) {
8324 this.documentHead.removeChild(script);
8325 }
8326
8327 return this;
8328 },
8329
8330 /**
8331 * Load a script file, supports both asynchronous and synchronous approaches
8332 * @private
8333 */
8334 loadScriptFile: function(url, onLoad, onError, scope, synchronous) {
8335 var me = this,
8336 isFileLoaded = this.isFileLoaded,
8337 scriptElements = this.scriptElements,
8338 noCacheUrl = url + (this.getConfig('disableCaching') ? ('?' + this.getConfig('disableCachingParam') + '=' + Ext.Date.now()) : ''),
8339 xhr, status, content, onScriptError;
8340
8341 if (isFileLoaded[url]) {
8342 return this;
8343 }
8344
8345 scope = scope || this;
8346
8347 this.isLoading = true;
8348
8349 if (!synchronous) {
8350 onScriptError = function() {
8351 //<debug error>
8352 onError.call(scope, "Failed loading '" + url + "', please verify that the file exists", synchronous);
8353 //</debug>
8354 };
8355
8356 if (!Ext.isReady && Ext.onDocumentReady) {
8357 Ext.onDocumentReady(function() {
8358 if (!isFileLoaded[url]) {
8359 scriptElements[url] = me.injectScriptElement(noCacheUrl, onLoad, onScriptError, scope);
8360 }
8361 });
8362 }
8363 else {
8364 scriptElements[url] = this.injectScriptElement(noCacheUrl, onLoad, onScriptError, scope);
8365 }
8366 }
8367 else {
8368 if (typeof XMLHttpRequest != 'undefined') {
8369 xhr = new XMLHttpRequest();
8370 } else {
8371 xhr = new ActiveXObject('Microsoft.XMLHTTP');
8372 }
8373
8374 try {
8375 xhr.open('GET', noCacheUrl, false);
8376 xhr.send(null);
8377 }
8378 catch (e) {
8379 //<debug error>
8380 onError.call(this, "Failed loading synchronously via XHR: '" + url + "'; It's likely that the file is either " +
8381 "being loaded from a different domain or from the local file system whereby cross origin " +
8382 "requests are not allowed due to security reasons. Use asynchronous loading with " +
8383 "Ext.require instead.", synchronous);
8384 //</debug>
8385 }
8386
8387 status = (xhr.status == 1223) ? 204 : xhr.status;
8388 content = xhr.responseText;
8389
8390 if ((status >= 200 && status < 300) || status == 304 || (status == 0 && content.length > 0)) {
8391 // Debugger friendly, file names are still shown even though they're eval'ed code
8392 // Breakpoints work on both Firebug and Chrome's Web Inspector
8393 Ext.globalEval(content + "\n//@ sourceURL=" + url);
8394 onLoad.call(scope);
8395 }
8396 else {
8397 //<debug>
8398 onError.call(this, "Failed loading synchronously via XHR: '" + url + "'; please " +
8399 "verify that the file exists. " +
8400 "XHR status code: " + status, synchronous);
8401 //</debug>
8402 }
8403
8404 // Prevent potential IE memory leak
8405 xhr = null;
8406 }
8407 },
8408
8409 // documented above
8410 syncRequire: function() {
8411 var syncModeEnabled = this.syncModeEnabled;
8412
8413 if (!syncModeEnabled) {
8414 this.syncModeEnabled = true;
8415 }
8416
8417 this.require.apply(this, arguments);
8418
8419 if (!syncModeEnabled) {
8420 this.syncModeEnabled = false;
8421 }
8422
8423 this.refreshQueue();
8424 },
8425
8426 // documented above
8427 require: function(expressions, fn, scope, excludes) {
8428 var excluded = {},
8429 included = {},
8430 queue = this.queue,
8431 classNameToFilePathMap = this.classNameToFilePathMap,
8432 isClassFileLoaded = this.isClassFileLoaded,
8433 excludedClassNames = [],
8434 possibleClassNames = [],
8435 classNames = [],
8436 references = [],
8437 callback,
8438 syncModeEnabled,
8439 filePath, expression, exclude, className,
8440 possibleClassName, i, j, ln, subLn;
8441
8442 if (excludes) {
8443 excludes = arrayFrom(excludes);
8444
8445 for (i = 0,ln = excludes.length; i < ln; i++) {
8446 exclude = excludes[i];
8447
8448 if (typeof exclude == 'string' && exclude.length > 0) {
8449 excludedClassNames = Manager.getNamesByExpression(exclude);
8450
8451 for (j = 0,subLn = excludedClassNames.length; j < subLn; j++) {
8452 excluded[excludedClassNames[j]] = true;
8453 }
8454 }
8455 }
8456 }
8457
8458 expressions = arrayFrom(expressions);
8459
8460 if (fn) {
8461 if (fn.length > 0) {
8462 callback = function() {
8463 var classes = [],
8464 i, ln, name;
8465
8466 for (i = 0,ln = references.length; i < ln; i++) {
8467 name = references[i];
8468 classes.push(Manager.get(name));
8469 }
8470
8471 return fn.apply(this, classes);
8472 };
8473 }
8474 else {
8475 callback = fn;
8476 }
8477 }
8478 else {
8479 callback = Ext.emptyFn;
8480 }
8481
8482 scope = scope || Ext.global;
8483
8484 for (i = 0,ln = expressions.length; i < ln; i++) {
8485 expression = expressions[i];
8486
8487 if (typeof expression == 'string' && expression.length > 0) {
8488 possibleClassNames = Manager.getNamesByExpression(expression);
8489 subLn = possibleClassNames.length;
8490
8491 for (j = 0; j < subLn; j++) {
8492 possibleClassName = possibleClassNames[j];
8493
8494 if (excluded[possibleClassName] !== true) {
8495 references.push(possibleClassName);
8496
8497 if (!Manager.isCreated(possibleClassName) && !included[possibleClassName]/* && !this.requiresMap.hasOwnProperty(possibleClassName)*/) {
8498 included[possibleClassName] = true;
8499 classNames.push(possibleClassName);
8500 }
8501 }
8502 }
8503 }
8504 }
8505
8506 // If the dynamic dependency feature is not being used, throw an error
8507 // if the dependencies are not defined
8508 if (classNames.length > 0) {
8509 if (!this.config.enabled) {
8510 throw new Error("Ext.Loader is not enabled, so dependencies cannot be resolved dynamically. " +
8511 "Missing required class" + ((classNames.length > 1) ? "es" : "") + ": " + classNames.join(', '));
8512 }
8513 }
8514 else {
8515 callback.call(scope);
8516 return this;
8517 }
8518
8519 syncModeEnabled = this.syncModeEnabled;
8520
8521 if (!syncModeEnabled) {
8522 queue.push({
8523 requires: classNames.slice(), // this array will be modified as the queue is processed,
8524 // so we need a copy of it
8525 callback: callback,
8526 scope: scope
8527 });
8528 }
8529
8530 ln = classNames.length;
8531
8532 for (i = 0; i < ln; i++) {
8533 className = classNames[i];
8534
8535 filePath = this.getPath(className);
8536
8537 // If we are synchronously loading a file that has already been asynchronously loaded before
8538 // we need to destroy the script tag and revert the count
8539 // This file will then be forced loaded in synchronous
8540 if (syncModeEnabled && isClassFileLoaded.hasOwnProperty(className)) {
8541 this.numPendingFiles--;
8542 this.removeScriptElement(filePath);
8543 delete isClassFileLoaded[className];
8544 }
8545
8546 if (!isClassFileLoaded.hasOwnProperty(className)) {
8547 isClassFileLoaded[className] = false;
8548
8549 classNameToFilePathMap[className] = filePath;
8550
8551 this.numPendingFiles++;
8552
8553 this.loadScriptFile(
8554 filePath,
8555 pass(this.onFileLoaded, [className, filePath], this),
8556 pass(this.onFileLoadError, [className, filePath]),
8557 this,
8558 syncModeEnabled
8559 );
8560 }
8561 }
8562
8563 if (syncModeEnabled) {
8564 callback.call(scope);
8565
8566 if (ln === 1) {
8567 return Manager.get(className);
8568 }
8569 }
8570
8571 return this;
8572 },
8573
8574 /**
8575 * @private
8576 * @param {String} className
8577 * @param {String} filePath
8578 */
8579 onFileLoaded: function(className, filePath) {
8580 this.numLoadedFiles++;
8581
8582 this.isClassFileLoaded[className] = true;
8583 this.isFileLoaded[filePath] = true;
8584
8585 this.numPendingFiles--;
8586
8587 if (this.numPendingFiles === 0) {
8588 this.refreshQueue();
8589 }
8590
8591 //<debug>
8592 if (!this.syncModeEnabled && this.numPendingFiles === 0 && this.isLoading && !this.hasFileLoadError) {
8593 var queue = this.queue,
8594 missingClasses = [],
8595 missingPaths = [],
8596 requires,
8597 i, ln, j, subLn;
8598
8599 for (i = 0,ln = queue.length; i < ln; i++) {
8600 requires = queue[i].requires;
8601
8602 for (j = 0,subLn = requires.length; j < subLn; j++) {
8603 if (this.isClassFileLoaded[requires[j]]) {
8604 missingClasses.push(requires[j]);
8605 }
8606 }
8607 }
8608
8609 if (missingClasses.length < 1) {
8610 return;
8611 }
8612
8613 missingClasses = Ext.Array.filter(Ext.Array.unique(missingClasses), function(item) {
8614 return !this.requiresMap.hasOwnProperty(item);
8615 }, this);
8616
8617 for (i = 0,ln = missingClasses.length; i < ln; i++) {
8618 missingPaths.push(this.classNameToFilePathMap[missingClasses[i]]);
8619 }
8620
8621 throw new Error("The following classes are not declared even if their files have been " +
8622 "loaded: '" + missingClasses.join("', '") + "'. Please check the source code of their " +
8623 "corresponding files for possible typos: '" + missingPaths.join("', '"));
8624 }
8625 //</debug>
8626 },
8627
8628 /**
8629 * @private
8630 */
8631 onFileLoadError: function(className, filePath, errorMessage, isSynchronous) {
8632 this.numPendingFiles--;
8633 this.hasFileLoadError = true;
8634
8635 //<debug error>
8636 throw new Error("[Ext.Loader] " + errorMessage);
8637 //</debug>
8638 },
8639
8640 /**
8641 * @private
8642 */
8643 addOptionalRequires: function(requires) {
8644 var optionalRequires = this.optionalRequires,
8645 i, ln, require;
8646
8647 requires = arrayFrom(requires);
8648
8649 for (i = 0, ln = requires.length; i < ln; i++) {
8650 require = requires[i];
8651
8652 arrayInclude(optionalRequires, require);
8653 }
8654
8655 return this;
8656 },
8657
8658 /**
8659 * @private
8660 */
8661 triggerReady: function(force) {
8662 var readyListeners = this.readyListeners,
8663 optionalRequires = this.optionalRequires,
8664 listener;
8665
8666 if (this.isLoading || force) {
8667 this.isLoading = false;
8668
8669 if (optionalRequires.length !== 0) {
8670 // Clone then empty the array to eliminate potential recursive loop issue
8671 optionalRequires = optionalRequires.slice();
8672
8673 // Empty the original array
8674 this.optionalRequires.length = 0;
8675
8676 this.require(optionalRequires, pass(this.triggerReady, [true], this), this);
8677 return this;
8678 }
8679
8680 while (readyListeners.length) {
8681 listener = readyListeners.shift();
8682 listener.fn.call(listener.scope);
8683
8684 if (this.isLoading) {
8685 return this;
8686 }
8687 }
8688 }
8689
8690 return this;
8691 },
8692
8693 // duplicate definition (documented above)
8694 onReady: function(fn, scope, withDomReady, options) {
8695 var oldFn;
8696
8697 if (withDomReady !== false && Ext.onDocumentReady) {
8698 oldFn = fn;
8699
8700 fn = function() {
8701 Ext.onDocumentReady(oldFn, scope, options);
8702 };
8703 }
8704
8705 if (!this.isLoading) {
8706 fn.call(scope);
8707 }
8708 else {
8709 this.readyListeners.push({
8710 fn: fn,
8711 scope: scope
8712 });
8713 }
8714 },
8715
8716 /**
8717 * @private
8718 * @param {String} className
8719 */
8720 historyPush: function(className) {
8721 var isInHistory = this.isInHistory;
8722
8723 if (className && this.isClassFileLoaded.hasOwnProperty(className) && !isInHistory[className]) {
8724 isInHistory[className] = true;
8725 this.history.push(className);
8726 }
8727
8728 return this;
8729 }
8730 });
8731
8732 //</feature>
8733
8734 /**
8735 * Convenient alias of {@link Ext.Loader#require}. Please see the introduction documentation of
8736 * {@link Ext.Loader} for examples.
8737 * @member Ext
8738 * @method require
8739 * @inheritdoc Ext.Loader#require
8740 */
8741 Ext.require = alias(Loader, 'require');
8742
8743 /**
8744 * Synchronous version of {@link Ext#require}, convenient alias of {@link Ext.Loader#syncRequire}.
8745 * @member Ext
8746 * @method syncRequire
8747 * @inheritdoc Ext.Loader#syncRequire
8748 */
8749 Ext.syncRequire = alias(Loader, 'syncRequire');
8750
8751 /**
8752 * Convenient shortcut to {@link Ext.Loader#exclude}.
8753 * @member Ext
8754 * @method exclude
8755 * @inheritdoc Ext.Loader#exclude
8756 */
8757 Ext.exclude = alias(Loader, 'exclude');
8758
8759 /**
8760 * Adds a listener to be notified when the document is ready and all dependencies are loaded.
8761 *
8762 * @param {Function} fn The method the event invokes.
8763 * @param {Object} [scope] The scope in which the handler function executes. Defaults to the browser window.
8764 * @param {Boolean} [options] Options object as passed to {@link Ext.Element#addListener}. It is recommended
8765 * that the options `{single: true}` be used so that the handler is removed on first invocation.
8766 * @member Ext
8767 * @method onReady
8768 */
8769 Ext.onReady = function(fn, scope, options) {
8770 Loader.onReady(fn, scope, true, options);
8771 };
8772
8773 Class.registerPreprocessor('loader', function(cls, data, hooks, continueFn) {
8774 var me = this,
8775 dependencies = [],
8776 className = Manager.getName(cls),
8777 i, j, ln, subLn, value, propertyName, propertyValue;
8778
8779 /*
8780 Loop through the dependencyProperties, look for string class names and push
8781 them into a stack, regardless of whether the property's value is a string, array or object. For example:
8782 {
8783 extend: 'Ext.MyClass',
8784 requires: ['Ext.some.OtherClass'],
8785 mixins: {
8786 observable: 'Ext.mixin.Observable';
8787 }
8788 }
8789 which will later be transformed into:
8790 {
8791 extend: Ext.MyClass,
8792 requires: [Ext.some.OtherClass],
8793 mixins: {
8794 observable: Ext.mixin.Observable;
8795 }
8796 }
8797 */
8798
8799 for (i = 0,ln = dependencyProperties.length; i < ln; i++) {
8800 propertyName = dependencyProperties[i];
8801
8802 if (data.hasOwnProperty(propertyName)) {
8803 propertyValue = data[propertyName];
8804
8805 if (typeof propertyValue == 'string') {
8806 dependencies.push(propertyValue);
8807 }
8808 else if (propertyValue instanceof Array) {
8809 for (j = 0, subLn = propertyValue.length; j < subLn; j++) {
8810 value = propertyValue[j];
8811
8812 if (typeof value == 'string') {
8813 dependencies.push(value);
8814 }
8815 }
8816 }
8817 else if (typeof propertyValue != 'function') {
8818 for (j in propertyValue) {
8819 if (propertyValue.hasOwnProperty(j)) {
8820 value = propertyValue[j];
8821
8822 if (typeof value == 'string') {
8823 dependencies.push(value);
8824 }
8825 }
8826 }
8827 }
8828 }
8829 }
8830
8831 if (dependencies.length === 0) {
8832 return;
8833 }
8834
8835 //<feature classSystem.loader>
8836 //<debug error>
8837 var deadlockPath = [],
8838 requiresMap = Loader.requiresMap,
8839 detectDeadlock;
8840
8841 /*
8842 Automatically detect deadlocks before-hand,
8843 will throw an error with detailed path for ease of debugging. Examples of deadlock cases:
8844
8845 - A extends B, then B extends A
8846 - A requires B, B requires C, then C requires A
8847
8848 The detectDeadlock function will recursively transverse till the leaf, hence it can detect deadlocks
8849 no matter how deep the path is.
8850 */
8851
8852 if (className) {
8853 requiresMap[className] = dependencies;
8854 //<debug>
8855 if (!Loader.requiredByMap) Loader.requiredByMap = {};
8856 Ext.Array.each(dependencies, function(dependency){
8857 if (!Loader.requiredByMap[dependency]) Loader.requiredByMap[dependency] = [];
8858 Loader.requiredByMap[dependency].push(className);
8859 });
8860 //</debug>
8861 detectDeadlock = function(cls) {
8862 deadlockPath.push(cls);
8863
8864 if (requiresMap[cls]) {
8865 if (Ext.Array.contains(requiresMap[cls], className)) {
8866 throw new Error("Deadlock detected while loading dependencies! '" + className + "' and '" +
8867 deadlockPath[1] + "' " + "mutually require each other. Path: " +
8868 deadlockPath.join(' -> ') + " -> " + deadlockPath[0]);
8869 }
8870
8871 for (i = 0,ln = requiresMap[cls].length; i < ln; i++) {
8872 detectDeadlock(requiresMap[cls][i]);
8873 }
8874 }
8875 };
8876
8877 detectDeadlock(className);
8878 }
8879
8880 //</debug>
8881 //</feature>
8882
8883 Loader.require(dependencies, function() {
8884 for (i = 0,ln = dependencyProperties.length; i < ln; i++) {
8885 propertyName = dependencyProperties[i];
8886
8887 if (data.hasOwnProperty(propertyName)) {
8888 propertyValue = data[propertyName];
8889
8890 if (typeof propertyValue == 'string') {
8891 data[propertyName] = Manager.get(propertyValue);
8892 }
8893 else if (propertyValue instanceof Array) {
8894 for (j = 0, subLn = propertyValue.length; j < subLn; j++) {
8895 value = propertyValue[j];
8896
8897 if (typeof value == 'string') {
8898 data[propertyName][j] = Manager.get(value);
8899 }
8900 }
8901 }
8902 else if (typeof propertyValue != 'function') {
8903 for (var k in propertyValue) {
8904 if (propertyValue.hasOwnProperty(k)) {
8905 value = propertyValue[k];
8906
8907 if (typeof value == 'string') {
8908 data[propertyName][k] = Manager.get(value);
8909 }
8910 }
8911 }
8912 }
8913 }
8914 }
8915
8916 continueFn.call(me, cls, data, hooks);
8917 });
8918
8919 return false;
8920 }, true, 'after', 'className');
8921
8922 //<feature classSystem.loader>
8923 /**
8924 * @cfg {String[]} uses
8925 * @member Ext.Class
8926 * List of optional classes to load together with this class. These aren't necessarily loaded before
8927 * this class is created, but are guaranteed to be available before Ext.onReady listeners are
8928 * invoked
8929 */
8930 Manager.registerPostprocessor('uses', function(name, cls, data) {
8931 var uses = arrayFrom(data.uses),
8932 items = [],
8933 i, ln, item;
8934
8935 for (i = 0,ln = uses.length; i < ln; i++) {
8936 item = uses[i];
8937
8938 if (typeof item == 'string') {
8939 items.push(item);
8940 }
8941 }
8942
8943 Loader.addOptionalRequires(items);
8944 });
8945
8946 Manager.onCreated(function(className) {
8947 this.historyPush(className);
8948 }, Loader);
8949 //</feature>
8950
8951 })(Ext.ClassManager, Ext.Class, Ext.Function.flexSetter, Ext.Function.alias,
8952 Ext.Function.pass, Ext.Array.from, Ext.Array.erase, Ext.Array.include);
8953
8954 // initalize the default path of the framework
8955 // trimmed down version of sench-touch-debug-suffix.js
8956 // with alias / alternates removed, as those are handled separately by
8957 // compiler-generated metadata
8958 (function() {
8959 var scripts = document.getElementsByTagName('script'),
8960 currentScript = scripts[scripts.length - 1],
8961 src = currentScript.src,
8962 path = src.substring(0, src.lastIndexOf('/') + 1),
8963 Loader = Ext.Loader;
8964
8965 //<debug>
8966 // if we're running in dev mode out of the repo src tree, then this
8967 // file will potentially be loaded from the touch/src/core/class folder
8968 // so we'll need to adjust for that
8969 if(src.indexOf("src/core/class/") != -1) {
8970 path = path + "../../../";
8971 }
8972 //</debug>
8973
8974
8975 Loader.setConfig({
8976 enabled: true,
8977 disableCaching: !/[?&](cache|breakpoint)/i.test(location.search),
8978 paths: {
8979 'Ext' : path + 'src'
8980 }
8981 });
8982
8983 })();
8984
8985 //@tag dom,core
8986 //@define Ext.EventManager
8987 //@define Ext.core.EventManager
8988 //@require Ext.Loader
8989
8990 /**
8991 * @class Ext.EventManager
8992 *
8993 * This object has been deprecated in Sencha Touch 2.0.0. Please refer to the method documentation for specific alternatives.
8994 *
8995 * @deprecated 2.0.0
8996 * @singleton
8997 * @private
8998 */
8999
9000
9001 //@tag dom,core
9002 //@define Ext-more
9003 //@require Ext.EventManager
9004
9005 /**
9006 * @class Ext
9007 *
9008 * Ext is the global namespace for the whole Sencha Touch framework. Every class, function and configuration for the
9009 * whole framework exists under this single global variable. The Ext singleton itself contains a set of useful helper
9010 * functions (like {@link #apply}, {@link #min} and others), but most of the framework that you use day to day exists
9011 * in specialized classes (for example {@link Ext.Panel}, {@link Ext.Carousel} and others).
9012 *
9013 * If you are new to Sencha Touch we recommend starting with the [Getting Started Guide][../../../getting_started/getting_started.html] to
9014 * get a feel for how the framework operates. After that, use the more focused guides on subjects like panels, forms and data
9015 * to broaden your understanding.
9016 *
9017 * The functions listed below are mostly utility functions used internally by many of the classes shipped in the
9018 * framework, but also often useful in your own apps.
9019 *
9020 * A method that is crucial to beginning your application is {@link #setup Ext.setup}. Please refer to it's documentation, or the
9021 * [Getting Started Guide][../../../getting_started/getting_started.html] as a reference on beginning your application.
9022 *
9023 * Ext.setup({
9024 * onReady: function() {
9025 * Ext.Viewport.add({
9026 * xtype: 'component',
9027 * html: 'Hello world!'
9028 * });
9029 * }
9030 * });
9031 *
9032 * ###Further Reading
9033 * [Getting Started Guide](../../../getting_started/getting_started.html)
9034 */
9035 Ext.setVersion('touch', '2.4.2.571');
9036
9037 Ext.apply(Ext, {
9038 /**
9039 * The version of the framework
9040 * @type String
9041 */
9042 version: Ext.getVersion('touch'),
9043
9044 /**
9045 * @private
9046 */
9047 idSeed: 0,
9048
9049 /**
9050 * Repaints the whole page. This fixes frequently encountered painting issues in mobile Safari.
9051 */
9052 repaint: function() {
9053 var mask = Ext.getBody().createChild({
9054 cls: Ext.baseCSSPrefix + 'mask ' + Ext.baseCSSPrefix + 'mask-transparent'
9055 });
9056 setTimeout(function() {
9057 mask.destroy();
9058 }, 0);
9059 },
9060
9061 /**
9062 * Generates unique ids. If the element is passes and it already has an `id`, it is unchanged.
9063 * @param {Mixed} el (optional) The element to generate an id for.
9064 * @param {String} [prefix=ext-gen] (optional) The `id` prefix.
9065 * @return {String} The generated `id`.
9066 */
9067 id: function(el, prefix) {
9068 if (el && el.id) {
9069 return el.id;
9070 }
9071
9072 el = Ext.getDom(el) || {};
9073
9074 if (el === document || el === document.documentElement) {
9075 el.id = 'ext-app';
9076 }
9077 else if (el === document.body) {
9078 el.id = 'ext-body';
9079 }
9080 else if (el === window) {
9081 el.id = 'ext-window';
9082 }
9083
9084 el.id = el.id || ((prefix || 'ext-') + (++Ext.idSeed));
9085
9086 return el.id;
9087 },
9088
9089 /**
9090 * Returns the current document body as an {@link Ext.Element}.
9091 * @return {Ext.Element} The document body.
9092 */
9093 getBody: function() {
9094 if (!Ext.documentBodyElement) {
9095 if (!document.body) {
9096 throw new Error("[Ext.getBody] document.body does not exist at this point");
9097 }
9098
9099 Ext.documentBodyElement = Ext.get(document.body);
9100 }
9101
9102 return Ext.documentBodyElement;
9103 },
9104
9105 /**
9106 * Returns the current document head as an {@link Ext.Element}.
9107 * @return {Ext.Element} The document head.
9108 */
9109 getHead: function() {
9110 if (!Ext.documentHeadElement) {
9111 Ext.documentHeadElement = Ext.get(document.head || document.getElementsByTagName('head')[0]);
9112 }
9113
9114 return Ext.documentHeadElement;
9115 },
9116
9117 /**
9118 * Returns the current HTML document object as an {@link Ext.Element}.
9119 * @return {Ext.Element} The document.
9120 */
9121 getDoc: function() {
9122 if (!Ext.documentElement) {
9123 Ext.documentElement = Ext.get(document);
9124 }
9125
9126 return Ext.documentElement;
9127 },
9128
9129 /**
9130 * This is shorthand reference to {@link Ext.ComponentMgr#get}.
9131 * Looks up an existing {@link Ext.Component Component} by {@link Ext.Component#getId id}
9132 * @param {String} id The component {@link Ext.Component#getId id}
9133 * @return {Ext.Component} The Component, `undefined` if not found, or `null` if a
9134 * Class was found.
9135 */
9136 getCmp: function(id) {
9137 return Ext.ComponentMgr.get(id);
9138 },
9139
9140 /**
9141 * Copies a set of named properties from the source object to the destination object.
9142 *
9143 * Example:
9144 *
9145 * ImageComponent = Ext.extend(Ext.Component, {
9146 * initComponent: function() {
9147 * this.autoEl = { tag: 'img' };
9148 * MyComponent.superclass.initComponent.apply(this, arguments);
9149 * this.initialBox = Ext.copyTo({}, this.initialConfig, 'x,y,width,height');
9150 * }
9151 * });
9152 *
9153 * Important note: To borrow class prototype methods, use {@link Ext.Base#borrow} instead.
9154 *
9155 * @param {Object} dest The destination object.
9156 * @param {Object} source The source object.
9157 * @param {String/String[]} names Either an Array of property names, or a comma-delimited list
9158 * of property names to copy.
9159 * @param {Boolean} [usePrototypeKeys=false] (optional) Pass `true` to copy keys off of the prototype as well as the instance.
9160 * @return {Object} The modified object.
9161 */
9162 copyTo : function(dest, source, names, usePrototypeKeys) {
9163 if (typeof names == 'string') {
9164 names = names.split(/[,;\s]/);
9165 }
9166 Ext.each (names, function(name) {
9167 if (usePrototypeKeys || source.hasOwnProperty(name)) {
9168 dest[name] = source[name];
9169 }
9170 }, this);
9171 return dest;
9172 },
9173
9174 /**
9175 * Attempts to destroy any objects passed to it by removing all event listeners, removing them from the
9176 * DOM (if applicable) and calling their destroy functions (if available). This method is primarily
9177 * intended for arguments of type {@link Ext.Element} and {@link Ext.Component}.
9178 * Any number of elements and/or components can be passed into this function in a single
9179 * call as separate arguments.
9180 * @param {Mixed...} args An {@link Ext.Element}, {@link Ext.Component}, or an Array of either of these to destroy.
9181 */
9182 destroy: function() {
9183 var args = arguments,
9184 ln = args.length,
9185 i, item;
9186
9187 for (i = 0; i < ln; i++) {
9188 item = args[i];
9189
9190 if (item) {
9191 if (Ext.isArray(item)) {
9192 this.destroy.apply(this, item);
9193 }
9194 else if (Ext.isFunction(item.destroy)) {
9195 item.destroy();
9196 }
9197 }
9198 }
9199 },
9200
9201 /**
9202 * Return the dom node for the passed String (id), dom node, or Ext.Element.
9203 * Here are some examples:
9204 *
9205 * // gets dom node based on id
9206 * var elDom = Ext.getDom('elId');
9207 *
9208 * // gets dom node based on the dom node
9209 * var elDom1 = Ext.getDom(elDom);
9210 *
9211 * // If we don't know if we are working with an
9212 * // Ext.Element or a dom node use Ext.getDom
9213 * function(el){
9214 * var dom = Ext.getDom(el);
9215 * // do something with the dom node
9216 * }
9217 *
9218 * __Note:__ the dom node to be found actually needs to exist (be rendered, etc)
9219 * when this method is called to be successful.
9220 * @param {Mixed} el
9221 * @return {HTMLElement}
9222 */
9223 getDom: function(el) {
9224 if (!el || !document) {
9225 return null;
9226 }
9227
9228 return el.dom ? el.dom : (typeof el == 'string' ? document.getElementById(el) : el);
9229 },
9230
9231 /**
9232 * Removes this element from the document, removes all DOM event listeners, and deletes the cache reference.
9233 * All DOM event listeners are removed from this element.
9234 * @param {HTMLElement} node The node to remove.
9235 */
9236 removeNode: function(node) {
9237 if (node && node.parentNode && node.tagName != 'BODY') {
9238 Ext.get(node).clearListeners();
9239 node.parentNode.removeChild(node);
9240 delete Ext.cache[node.id];
9241 }
9242 },
9243
9244 /**
9245 * @private
9246 */
9247 defaultSetupConfig: {
9248 eventPublishers: {
9249 dom: {
9250 xclass: 'Ext.event.publisher.Dom'
9251 },
9252 touchGesture: {
9253 xclass: 'Ext.event.publisher.TouchGesture',
9254 recognizers: {
9255 drag: {
9256 xclass: 'Ext.event.recognizer.Drag'
9257 },
9258 tap: {
9259 xclass: 'Ext.event.recognizer.Tap'
9260 },
9261 doubleTap: {
9262 xclass: 'Ext.event.recognizer.DoubleTap'
9263 },
9264 longPress: {
9265 xclass: 'Ext.event.recognizer.LongPress'
9266 },
9267 swipe: {
9268 xclass: 'Ext.event.recognizer.Swipe'
9269 },
9270 pinch: {
9271 xclass: 'Ext.event.recognizer.Pinch'
9272 },
9273 rotate: {
9274 xclass: 'Ext.event.recognizer.Rotate'
9275 },
9276 edgeSwipe: {
9277 xclass: 'Ext.event.recognizer.EdgeSwipe'
9278 }
9279 }
9280 },
9281 componentDelegation: {
9282 xclass: 'Ext.event.publisher.ComponentDelegation'
9283 },
9284 componentPaint: {
9285 xclass: 'Ext.event.publisher.ComponentPaint'
9286 },
9287 // componentSize: {
9288 // xclass: 'Ext.event.publisher.ComponentSize'
9289 // },
9290 elementPaint: {
9291 xclass: 'Ext.event.publisher.ElementPaint'
9292 },
9293 elementSize: {
9294 xclass: 'Ext.event.publisher.ElementSize'
9295 }
9296 //<feature charts>
9297 ,seriesItemEvents: {
9298 xclass: 'Ext.chart.series.ItemPublisher'
9299 }
9300 //</feature>
9301 },
9302
9303 //<feature logger>
9304 logger: {
9305 enabled: true,
9306 xclass: 'Ext.log.Logger',
9307 minPriority: 'deprecate',
9308 writers: {
9309 console: {
9310 xclass: 'Ext.log.writer.Console',
9311 throwOnErrors: true,
9312 formatter: {
9313 xclass: 'Ext.log.formatter.Default'
9314 }
9315 }
9316 }
9317 },
9318 //</feature>
9319
9320 animator: {
9321 xclass: 'Ext.fx.Runner'
9322 },
9323
9324 viewport: {
9325 xclass: 'Ext.viewport.Viewport'
9326 }
9327 },
9328
9329 /**
9330 * @private
9331 */
9332 isSetup: false,
9333
9334 /**
9335 * This indicate the start timestamp of current cycle.
9336 * It is only reliable during dom-event-initiated cycles and
9337 * {@link Ext.draw.Animator} initiated cycles.
9338 */
9339 frameStartTime: +new Date(),
9340
9341 /**
9342 * @private
9343 */
9344 setupListeners: [],
9345
9346 /**
9347 * @private
9348 */
9349 onSetup: function(fn, scope) {
9350 if (Ext.isSetup) {
9351 fn.call(scope);
9352 }
9353 else {
9354 Ext.setupListeners.push({
9355 fn: fn,
9356 scope: scope
9357 });
9358 }
9359 },
9360
9361 /**
9362 * Ext.setup() is the entry-point to initialize a Sencha Touch application. Note that if your application makes
9363 * use of MVC architecture, use {@link Ext#application} instead.
9364 *
9365 * This method accepts one single argument in object format. The most basic use of Ext.setup() is as follows:
9366 *
9367 * Ext.setup({
9368 * onReady: function() {
9369 * // ...
9370 * }
9371 * });
9372 *
9373 * This sets up the viewport, initializes the event system, instantiates a default animation runner, and a default
9374 * logger (during development). When all of that is ready, it invokes the callback function given to the `onReady` key.
9375 *
9376 * The default scope (`this`) of `onReady` is the main viewport. By default the viewport instance is stored in
9377 * {@link Ext.Viewport}. For example, this snippet adds a 'Hello World' button that is centered on the screen:
9378 *
9379 * Ext.setup({
9380 * onReady: function() {
9381 * this.add({
9382 * xtype: 'button',
9383 * centered: true,
9384 * text: 'Hello world!'
9385 * }); // Equivalent to Ext.Viewport.add(...)
9386 * }
9387 * });
9388 *
9389 * @param {Object} config An object with the following config options:
9390 *
9391 * @param {Function} config.onReady
9392 * A function to be called when the application is ready. Your application logic should be here.
9393 *
9394 * @param {Object} config.viewport
9395 * A custom config object to be used when creating the global {@link Ext.Viewport} instance. Please refer to the
9396 * {@link Ext.Viewport} documentation for more information.
9397 *
9398 * Ext.setup({
9399 * viewport: {
9400 * width: 500,
9401 * height: 500
9402 * },
9403 * onReady: function() {
9404 * // ...
9405 * }
9406 * });
9407 *
9408 * @param {String/Object} config.icon
9409 * Specifies a set of URLs to the application icon for different device form factors. This icon is displayed
9410 * when the application is added to the device's Home Screen.
9411 *
9412 * Ext.setup({
9413 * icon: {
9414 * 57: 'resources/icons/Icon.png',
9415 * 72: 'resources/icons/Icon~ipad.png',
9416 * 114: 'resources/icons/Icon@2x.png',
9417 * 144: 'resources/icons/Icon~ipad@2x.png'
9418 * },
9419 * onReady: function() {
9420 * // ...
9421 * }
9422 * });
9423 *
9424 * Each key represents the dimension of the icon as a square shape. For example: '57' is the key for a 57 x 57
9425 * icon image. Here is the breakdown of each dimension and its device target:
9426 *
9427 * - 57: Non-retina iPhone, iPod touch, and all Android devices
9428 * - 72: Retina iPhone and iPod touch
9429 * - 114: Non-retina iPad (first and second generation)
9430 * - 144: Retina iPad (third generation)
9431 *
9432 * Note that the dimensions of the icon images must be exactly 57x57, 72x72, 114x114 and 144x144 respectively.
9433 *
9434 * It is highly recommended that you provide all these different sizes to accommodate a full range of
9435 * devices currently available. However if you only have one icon in one size, make it 57x57 in size and
9436 * specify it as a string value. This same icon will be used on all supported devices.
9437 *
9438 * Ext.setup({
9439 * icon: 'resources/icons/Icon.png',
9440 * onReady: function() {
9441 * // ...
9442 * }
9443 * });
9444 *
9445 * @param {Object} config.startupImage
9446 * Specifies a set of URLs to the application startup images for different device form factors. This image is
9447 * displayed when the application is being launched from the Home Screen icon. Note that this currently only applies
9448 * to iOS devices.
9449 *
9450 * Ext.setup({
9451 * startupImage: {
9452 * '320x460': 'resources/startup/320x460.jpg',
9453 * '640x920': 'resources/startup/640x920.png',
9454 * '640x1096': 'resources/startup/640x1096.png',
9455 * '768x1004': 'resources/startup/768x1004.png',
9456 * '748x1024': 'resources/startup/748x1024.png',
9457 * '1536x2008': 'resources/startup/1536x2008.png',
9458 * '1496x2048': 'resources/startup/1496x2048.png'
9459 * },
9460 * onReady: function() {
9461 * // ...
9462 * }
9463 * });
9464 *
9465 * Each key represents the dimension of the image. For example: '320x460' is the key for a 320px x 460px image.
9466 * Here is the breakdown of each dimension and its device target:
9467 *
9468 * - 320x460: Non-retina iPhone, iPod touch, and all Android devices
9469 * - 640x920: Retina iPhone and iPod touch
9470 * - 640x1096: iPhone 5 and iPod touch (fifth generation)
9471 * - 768x1004: Non-retina iPad (first and second generation) in portrait orientation
9472 * - 748x1024: Non-retina iPad (first and second generation) in landscape orientation
9473 * - 1536x2008: Retina iPad (third generation) in portrait orientation
9474 * - 1496x2048: Retina iPad (third generation) in landscape orientation
9475 *
9476 * Please note that there's no automatic fallback mechanism for the startup images. In other words, if you don't specify
9477 * a valid image for a certain device, nothing will be displayed while the application is being launched on that device.
9478 *
9479 * @param {Boolean} config.isIconPrecomposed
9480 * True to not having a glossy effect added to the icon by the OS, which will preserve its exact look. This currently
9481 * only applies to iOS devices.
9482 *
9483 * @param {String} config.statusBarStyle
9484 * The style of status bar to be shown on applications added to the iOS home screen. Valid options are:
9485 *
9486 * * `default`
9487 * * `black`
9488 * * `black-translucent`
9489 *
9490 * @param {String[]} config.requires
9491 * An array of required classes for your application which will be automatically loaded before `onReady` is invoked.
9492 * Please refer to {@link Ext.Loader} and {@link Ext.Loader#require} for more information.
9493 *
9494 * Ext.setup({
9495 * requires: ['Ext.Button', 'Ext.tab.Panel'],
9496 * onReady: function() {
9497 * // ...
9498 * }
9499 * });
9500 *
9501 * @param {Object} config.eventPublishers
9502 * Sencha Touch, by default, includes various {@link Ext.event.recognizer.Recognizer} subclasses to recognize events fired
9503 * in your application. The list of default recognizers can be found in the documentation for
9504 * {@link Ext.event.recognizer.Recognizer}.
9505 *
9506 * To change the default recognizers, you can use the following syntax:
9507 *
9508 * Ext.setup({
9509 * eventPublishers: {
9510 * touchGesture: {
9511 * recognizers: {
9512 * swipe: {
9513 * // this will include both vertical and horizontal swipe recognizers
9514 * xclass: 'Ext.event.recognizer.Swipe'
9515 * }
9516 * }
9517 * }
9518 * },
9519 * onReady: function() {
9520 * // ...
9521 * }
9522 * });
9523 *
9524 * You can also disable recognizers using this syntax:
9525 *
9526 * Ext.setup({
9527 * eventPublishers: {
9528 * touchGesture: {
9529 * recognizers: {
9530 * swipe: null,
9531 * pinch: null,
9532 * rotate: null
9533 * }
9534 * }
9535 * },
9536 * onReady: function() {
9537 * // ...
9538 * }
9539 * });
9540 */
9541 setup: function(config) {
9542 var defaultSetupConfig = Ext.defaultSetupConfig,
9543 emptyFn = Ext.emptyFn,
9544 onReady = config.onReady || emptyFn,
9545 onUpdated = config.onUpdated || emptyFn,
9546 scope = config.scope,
9547 requires = Ext.Array.from(config.requires),
9548 extOnReady = Ext.onReady,
9549 head = Ext.getHead(),
9550 callback, viewport, precomposed;
9551
9552 Ext.setup = function() {
9553 throw new Error("Ext.setup has already been called before");
9554 };
9555
9556 delete config.requires;
9557 delete config.onReady;
9558 delete config.onUpdated;
9559 delete config.scope;
9560
9561 Ext.require(['Ext.event.Dispatcher']);
9562
9563 callback = function() {
9564 var listeners = Ext.setupListeners,
9565 ln = listeners.length,
9566 i, listener;
9567
9568 delete Ext.setupListeners;
9569 Ext.isSetup = true;
9570
9571 for (i = 0; i < ln; i++) {
9572 listener = listeners[i];
9573 listener.fn.call(listener.scope);
9574 }
9575
9576 Ext.onReady = extOnReady;
9577 Ext.onReady(onReady, scope);
9578 };
9579
9580 Ext.onUpdated = onUpdated;
9581 Ext.onReady = function(fn, scope) {
9582 var origin = onReady;
9583
9584 onReady = function() {
9585 origin();
9586 Ext.onReady(fn, scope);
9587 };
9588 };
9589
9590 config = Ext.merge({}, defaultSetupConfig, config);
9591
9592 Ext.onDocumentReady(function() {
9593 Ext.factoryConfig(config, function(data) {
9594 Ext.event.Dispatcher.getInstance().setPublishers(data.eventPublishers);
9595
9596 if (data.logger) {
9597 Ext.Logger = data.logger;
9598 }
9599
9600 if (data.animator) {
9601 Ext.Animator = data.animator;
9602 }
9603
9604 if (data.viewport) {
9605 Ext.Viewport = viewport = data.viewport;
9606
9607 if (!scope) {
9608 scope = viewport;
9609 }
9610
9611 Ext.require(requires, function() {
9612 Ext.Viewport.on('ready', callback, null, {single: true});
9613 });
9614 }
9615 else {
9616 Ext.require(requires, callback);
9617 }
9618 });
9619
9620 if (!Ext.microloaded && navigator.userAgent.match(/IEMobile\/10\.0/)) {
9621 var msViewportStyle = document.createElement("style");
9622 msViewportStyle.appendChild(
9623 document.createTextNode(
9624 "@media screen and (orientation: portrait) {" +
9625 "@-ms-viewport {width: 320px !important;}" +
9626 "}" +
9627 "@media screen and (orientation: landscape) {" +
9628 "@-ms-viewport {width: 560px !important;}" +
9629 "}"
9630 )
9631 );
9632 head.appendChild(msViewportStyle);
9633 }
9634 });
9635
9636 function addMeta(name, content) {
9637 var meta = document.createElement('meta');
9638
9639 meta.setAttribute('name', name);
9640 meta.setAttribute('content', content);
9641 head.append(meta);
9642 }
9643
9644 function addIcon(href, sizes, precomposed) {
9645 var link = document.createElement('link');
9646 link.setAttribute('rel', 'apple-touch-icon' + (precomposed ? '-precomposed' : ''));
9647 link.setAttribute('href', href);
9648 if (sizes) {
9649 link.setAttribute('sizes', sizes);
9650 }
9651 head.append(link);
9652 }
9653
9654 function addStartupImage(href, media) {
9655 var link = document.createElement('link');
9656 link.setAttribute('rel', 'apple-touch-startup-image');
9657 link.setAttribute('href', href);
9658 if (media) {
9659 link.setAttribute('media', media);
9660 }
9661 head.append(link);
9662 }
9663
9664 var icon = config.icon,
9665 isIconPrecomposed = Boolean(config.isIconPrecomposed),
9666 startupImage = config.startupImage || {},
9667 statusBarStyle = config.statusBarStyle || 'black',
9668 devicePixelRatio = window.devicePixelRatio || 1;
9669
9670
9671 if (navigator.standalone) {
9672 addMeta('viewport', 'width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0');
9673 }
9674 else {
9675 addMeta('viewport', 'initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, minimum-ui');
9676 }
9677 addMeta('apple-mobile-web-app-capable', 'yes');
9678 addMeta('apple-touch-fullscreen', 'yes');
9679 if (Ext.browser.is.ie) {
9680 addMeta('msapplication-tap-highlight', 'no');
9681 }
9682
9683 // status bar style
9684 if (statusBarStyle) {
9685 addMeta('apple-mobile-web-app-status-bar-style', statusBarStyle);
9686 }
9687
9688 if (Ext.isString(icon)) {
9689 icon = {
9690 57: icon,
9691 72: icon,
9692 114: icon,
9693 144: icon
9694 };
9695 }
9696 else if (!icon) {
9697 icon = {};
9698 }
9699
9700
9701 if (Ext.os.is.iPad) {
9702 if (devicePixelRatio >= 2) {
9703 // Retina iPad - Landscape
9704 if ('1496x2048' in startupImage) {
9705 addStartupImage(startupImage['1496x2048'], '(orientation: landscape)');
9706 }
9707 // Retina iPad - Portrait
9708 if ('1536x2008' in startupImage) {
9709 addStartupImage(startupImage['1536x2008'], '(orientation: portrait)');
9710 }
9711
9712 // Retina iPad
9713 if ('144' in icon) {
9714 addIcon(icon['144'], '144x144', isIconPrecomposed);
9715 }
9716 }
9717 else {
9718 // Non-Retina iPad - Landscape
9719 if ('748x1024' in startupImage) {
9720 addStartupImage(startupImage['748x1024'], '(orientation: landscape)');
9721 }
9722 // Non-Retina iPad - Portrait
9723 if ('768x1004' in startupImage) {
9724 addStartupImage(startupImage['768x1004'], '(orientation: portrait)');
9725 }
9726
9727 // Non-Retina iPad
9728 if ('72' in icon) {
9729 addIcon(icon['72'], '72x72', isIconPrecomposed);
9730 }
9731 }
9732 }
9733 else {
9734 // Retina iPhone, iPod touch with iOS version >= 4.3
9735 if (devicePixelRatio >= 2 && Ext.os.version.gtEq('4.3')) {
9736 if (Ext.os.is.iPhone5) {
9737 addStartupImage(startupImage['640x1096']);
9738 } else {
9739 addStartupImage(startupImage['640x920']);
9740 }
9741
9742 // Retina iPhone and iPod touch
9743 if ('114' in icon) {
9744 addIcon(icon['114'], '114x114', isIconPrecomposed);
9745 }
9746 }
9747 else {
9748 addStartupImage(startupImage['320x460']);
9749
9750 // Non-Retina iPhone, iPod touch, and Android devices
9751 if ('57' in icon) {
9752 addIcon(icon['57'], null, isIconPrecomposed);
9753 }
9754 }
9755 }
9756 },
9757
9758 /**
9759 * @member Ext
9760 * @method application
9761 *
9762 * Loads Ext.app.Application class and starts it up with given configuration after the page is ready.
9763 *
9764 * Ext.application({
9765 * launch: function() {
9766 * alert('Application launched!');
9767 * }
9768 * });
9769 *
9770 * See {@link Ext.app.Application} for details.
9771 *
9772 * @param {Object} config An object with the following config options:
9773 *
9774 * @param {Function} config.launch
9775 * A function to be called when the application is ready. Your application logic should be here. Please see {@link Ext.app.Application}
9776 * for details.
9777 *
9778 * @param {Object} config.viewport
9779 * An object to be used when creating the global {@link Ext.Viewport} instance. Please refer to the {@link Ext.Viewport}
9780 * documentation for more information.
9781 *
9782 * Ext.application({
9783 * viewport: {
9784 * layout: 'vbox'
9785 * },
9786 * launch: function() {
9787 * Ext.Viewport.add({
9788 * flex: 1,
9789 * html: 'top (flex: 1)'
9790 * });
9791 *
9792 * Ext.Viewport.add({
9793 * flex: 4,
9794 * html: 'bottom (flex: 4)'
9795 * });
9796 * }
9797 * });
9798 *
9799 * @param {String/Object} config.icon
9800 * Specifies a set of URLs to the application icon for different device form factors. This icon is displayed
9801 * when the application is added to the device's Home Screen.
9802 *
9803 * Ext.application({
9804 * icon: {
9805 * 57: 'resources/icons/Icon.png',
9806 * 72: 'resources/icons/Icon~ipad.png',
9807 * 114: 'resources/icons/Icon@2x.png',
9808 * 144: 'resources/icons/Icon~ipad@2x.png'
9809 * },
9810 * launch: function() {
9811 * // ...
9812 * }
9813 * });
9814 *
9815 * Each key represents the dimension of the icon as a square shape. For example: '57' is the key for a 57 x 57
9816 * icon image. Here is the breakdown of each dimension and its device target:
9817 *
9818 * - 57: Non-retina iPhone, iPod touch, and all Android devices
9819 * - 72: Retina iPhone and iPod touch
9820 * - 114: Non-retina iPad (first and second generation)
9821 * - 144: Retina iPad (third generation)
9822 *
9823 * Note that the dimensions of the icon images must be exactly 57x57, 72x72, 114x114 and 144x144 respectively.
9824 *
9825 * It is highly recommended that you provide all these different sizes to accommodate a full range of
9826 * devices currently available. However if you only have one icon in one size, make it 57x57 in size and
9827 * specify it as a string value. This same icon will be used on all supported devices.
9828 *
9829 * Ext.setup({
9830 * icon: 'resources/icons/Icon.png',
9831 * onReady: function() {
9832 * // ...
9833 * }
9834 * });
9835 *
9836 * @param {Object} config.startupImage
9837 * Specifies a set of URLs to the application startup images for different device form factors. This image is
9838 * displayed when the application is being launched from the Home Screen icon. Note that this currently only applies
9839 * to iOS devices.
9840 *
9841 * Ext.application({
9842 * startupImage: {
9843 * '320x460': 'resources/startup/320x460.jpg',
9844 * '640x920': 'resources/startup/640x920.png',
9845 * '640x1096': 'resources/startup/640x1096.png',
9846 * '768x1004': 'resources/startup/768x1004.png',
9847 * '748x1024': 'resources/startup/748x1024.png',
9848 * '1536x2008': 'resources/startup/1536x2008.png',
9849 * '1496x2048': 'resources/startup/1496x2048.png'
9850 * },
9851 * launch: function() {
9852 * // ...
9853 * }
9854 * });
9855 *
9856 * Each key represents the dimension of the image. For example: '320x460' is the key for a 320px x 460px image.
9857 * Here is the breakdown of each dimension and its device target:
9858 *
9859 * - 320x460: Non-retina iPhone, iPod touch, and all Android devices
9860 * - 640x920: Retina iPhone and iPod touch
9861 * - 640x1096: iPhone 5 and iPod touch (fifth generation)
9862 * - 768x1004: Non-retina iPad (first and second generation) in portrait orientation
9863 * - 748x1024: Non-retina iPad (first and second generation) in landscape orientation
9864 * - 1536x2008: Retina iPad (third generation) in portrait orientation
9865 * - 1496x2048: Retina iPad (third generation) in landscape orientation
9866 *
9867 * Please note that there's no automatic fallback mechanism for the startup images. In other words, if you don't specify
9868 * a valid image for a certain device, nothing will be displayed while the application is being launched on that device.
9869 *
9870 * @param {Boolean} config.isIconPrecomposed
9871 * True to not having a glossy effect added to the icon by the OS, which will preserve its exact look. This currently
9872 * only applies to iOS devices.
9873 *
9874 * @param {String} config.statusBarStyle
9875 * The style of status bar to be shown on applications added to the iOS home screen. Valid options are:
9876 *
9877 * * `default`
9878 * * `black`
9879 * * `black-translucent`
9880 *
9881 * @param {String[]} config.requires
9882 * An array of required classes for your application which will be automatically loaded if {@link Ext.Loader#enabled} is set
9883 * to `true`. Please refer to {@link Ext.Loader} and {@link Ext.Loader#require} for more information.
9884 *
9885 * Ext.application({
9886 * requires: ['Ext.Button', 'Ext.tab.Panel'],
9887 * launch: function() {
9888 * // ...
9889 * }
9890 * });
9891 *
9892 * @param {Object} config.eventPublishers
9893 * Sencha Touch, by default, includes various {@link Ext.event.recognizer.Recognizer} subclasses to recognize events fired
9894 * in your application. The list of default recognizers can be found in the documentation for {@link Ext.event.recognizer.Recognizer}.
9895 *
9896 * To change the default recognizers, you can use the following syntax:
9897 *
9898 * Ext.application({
9899 * eventPublishers: {
9900 * touchGesture: {
9901 * recognizers: {
9902 * swipe: {
9903 * // this will include both vertical and horizontal swipe recognizers
9904 * xclass: 'Ext.event.recognizer.Swipe'
9905 * }
9906 * }
9907 * }
9908 * },
9909 * launch: function() {
9910 * // ...
9911 * }
9912 * });
9913 *
9914 * You can also disable recognizers using this syntax:
9915 *
9916 * Ext.application({
9917 * eventPublishers: {
9918 * touchGesture: {
9919 * recognizers: {
9920 * swipe: null,
9921 * pinch: null,
9922 * rotate: null
9923 * }
9924 * }
9925 * },
9926 * launch: function() {
9927 * // ...
9928 * }
9929 * });
9930 *
9931 * @param {Function} config.onUpdated
9932 * This function will execute once the production microloader determines there are updates to the application and has
9933 * merged the updates into the application. You can then alert the user there was an update and can then reload
9934 * the application to execute the updated application.
9935 *
9936 * Ext.application({
9937 * onUpdated : function() {
9938 * Ext.Msg.confirm(
9939 * 'Application Update',
9940 * 'This application has just successfully been updated to the latest version. Reload now?',
9941 * function(buttonId) {
9942 * if (buttonId === 'yes') {
9943 * window.location.reload();
9944 * }
9945 * }
9946 * );
9947 * }
9948 * });
9949 */
9950 application: function(config) {
9951 var appName = config.name,
9952 onReady, scope, requires;
9953
9954 if (!config) {
9955 config = {};
9956 }
9957
9958 if (!Ext.Loader.config.paths[appName]) {
9959 Ext.Loader.setPath(appName, config.appFolder || 'app');
9960 }
9961
9962 requires = Ext.Array.from(config.requires);
9963 config.requires = ['Ext.app.Application'];
9964
9965 onReady = config.onReady;
9966 scope = config.scope;
9967
9968 config.onReady = function() {
9969 config.requires = requires;
9970 new Ext.app.Application(config);
9971
9972 if (onReady) {
9973 onReady.call(scope);
9974 }
9975 };
9976
9977 Ext.setup(config);
9978 },
9979
9980 /**
9981 * @private
9982 * @param {Object} config
9983 * @param {Function} callback
9984 * @member Ext
9985 */
9986 factoryConfig: function(config, callback) {
9987 var isSimpleObject = Ext.isSimpleObject(config);
9988
9989 if (isSimpleObject && config.xclass) {
9990 var className = config.xclass;
9991
9992 delete config.xclass;
9993
9994 Ext.require(className, function() {
9995 Ext.factoryConfig(config, function(cfg) {
9996 callback(Ext.create(className, cfg));
9997 });
9998 });
9999
10000 return;
10001 }
10002
10003 var isArray = Ext.isArray(config),
10004 keys = [],
10005 key, value, i, ln;
10006
10007 if (isSimpleObject || isArray) {
10008 if (isSimpleObject) {
10009 for (key in config) {
10010 if (config.hasOwnProperty(key)) {
10011 value = config[key];
10012 if (Ext.isSimpleObject(value) || Ext.isArray(value)) {
10013 keys.push(key);
10014 }
10015 }
10016 }
10017 }
10018 else {
10019 for (i = 0,ln = config.length; i < ln; i++) {
10020 value = config[i];
10021
10022 if (Ext.isSimpleObject(value) || Ext.isArray(value)) {
10023 keys.push(i);
10024 }
10025 }
10026 }
10027
10028 i = 0;
10029 ln = keys.length;
10030
10031 if (ln === 0) {
10032 callback(config);
10033 return;
10034 }
10035
10036 function fn(value) {
10037 config[key] = value;
10038 i++;
10039 factory();
10040 }
10041
10042 function factory() {
10043 if (i >= ln) {
10044 callback(config);
10045 return;
10046 }
10047
10048 key = keys[i];
10049 value = config[key];
10050
10051 Ext.factoryConfig(value, fn);
10052 }
10053
10054 factory();
10055 return;
10056 }
10057
10058 callback(config);
10059 },
10060
10061 /**
10062 * A global factory method to instantiate a class from a config object. For example, these two calls are equivalent:
10063 *
10064 * Ext.factory({ text: 'My Button' }, 'Ext.Button');
10065 * Ext.create('Ext.Button', { text: 'My Button' });
10066 *
10067 * If an existing instance is also specified, it will be updated with the supplied config object. This is useful
10068 * if you need to either create or update an object, depending on if an instance already exists. For example:
10069 *
10070 * var button;
10071 * button = Ext.factory({ text: 'New Button' }, 'Ext.Button', button); // Button created
10072 * button = Ext.factory({ text: 'Updated Button' }, 'Ext.Button', button); // Button updated
10073 *
10074 * @param {Object} config The config object to instantiate or update an instance with.
10075 * @param {String} classReference The class to instantiate from.
10076 * @param {Object} [instance] The instance to update.
10077 * @param {String} [aliasNamespace]
10078 * @member Ext
10079 */
10080 factory: function(config, classReference, instance, aliasNamespace) {
10081 var manager = Ext.ClassManager,
10082 newInstance;
10083
10084 // If config is falsy or a valid instance, destroy the current instance
10085 // (if it exists) and replace with the new one
10086 if (!config || config.isInstance) {
10087 if (instance && instance !== config) {
10088 instance.destroy();
10089 }
10090
10091 return config;
10092 }
10093
10094 if (aliasNamespace) {
10095 // If config is a string value, treat it as an alias
10096 if (typeof config == 'string') {
10097 return manager.instantiateByAlias(aliasNamespace + '.' + config);
10098 }
10099 // Same if 'type' is given in config
10100 else if (Ext.isObject(config) && 'type' in config) {
10101 return manager.instantiateByAlias(aliasNamespace + '.' + config.type, config);
10102 }
10103 }
10104
10105 if (config === true) {
10106 return instance || manager.instantiate(classReference);
10107 }
10108
10109 //<debug error>
10110 if (!Ext.isObject(config)) {
10111 Ext.Logger.error("Invalid config, must be a valid config object");
10112 }
10113 //</debug>
10114
10115 if ('xtype' in config) {
10116 newInstance = manager.instantiateByAlias('widget.' + config.xtype, config);
10117 }
10118 else if ('xclass' in config) {
10119 newInstance = manager.instantiate(config.xclass, config);
10120 }
10121
10122 if (newInstance) {
10123 if (instance) {
10124 instance.destroy();
10125 }
10126
10127 return newInstance;
10128 }
10129
10130 if (instance) {
10131 return instance.setConfig(config);
10132 }
10133
10134 return manager.instantiate(classReference, config);
10135 },
10136
10137 /**
10138 * @private
10139 * @member Ext
10140 */
10141 deprecateClassMember: function(cls, oldName, newName, message) {
10142 return this.deprecateProperty(cls.prototype, oldName, newName, message);
10143 },
10144
10145 /**
10146 * @private
10147 * @member Ext
10148 */
10149 deprecateClassMembers: function(cls, members) {
10150 var prototype = cls.prototype,
10151 oldName, newName;
10152
10153 for (oldName in members) {
10154 if (members.hasOwnProperty(oldName)) {
10155 newName = members[oldName];
10156
10157 this.deprecateProperty(prototype, oldName, newName);
10158 }
10159 }
10160 },
10161
10162 /**
10163 * @private
10164 * @member Ext
10165 */
10166 deprecateProperty: function(object, oldName, newName, message) {
10167 if (!message) {
10168 message = "'" + oldName + "' is deprecated";
10169 }
10170 if (newName) {
10171 message += ", please use '" + newName + "' instead";
10172 }
10173
10174 if (newName) {
10175 Ext.Object.defineProperty(object, oldName, {
10176 get: function() {
10177 //<debug warn>
10178 Ext.Logger.deprecate(message, 1);
10179 //</debug>
10180 return this[newName];
10181 },
10182 set: function(value) {
10183 //<debug warn>
10184 Ext.Logger.deprecate(message, 1);
10185 //</debug>
10186
10187 this[newName] = value;
10188 },
10189 configurable: true
10190 });
10191 }
10192 },
10193
10194 /**
10195 * @private
10196 * @member Ext
10197 */
10198 deprecatePropertyValue: function(object, name, value, message) {
10199 Ext.Object.defineProperty(object, name, {
10200 get: function() {
10201 //<debug warn>
10202 Ext.Logger.deprecate(message, 1);
10203 //</debug>
10204 return value;
10205 },
10206 configurable: true
10207 });
10208 },
10209
10210 /**
10211 * @private
10212 * @member Ext
10213 */
10214 deprecateMethod: function(object, name, method, message) {
10215 object[name] = function() {
10216 //<debug warn>
10217 Ext.Logger.deprecate(message, 2);
10218 //</debug>
10219 if (method) {
10220 return method.apply(this, arguments);
10221 }
10222 };
10223 },
10224
10225 /**
10226 * @private
10227 * @member Ext
10228 */
10229 deprecateClassMethod: function(cls, name, method, message) {
10230 if (typeof name != 'string') {
10231 var from, to;
10232
10233 for (from in name) {
10234 if (name.hasOwnProperty(from)) {
10235 to = name[from];
10236 Ext.deprecateClassMethod(cls, from, to);
10237 }
10238 }
10239 return;
10240 }
10241
10242 var isLateBinding = typeof method == 'string',
10243 member;
10244
10245 if (!message) {
10246 message = "'" + name + "()' is deprecated, please use '" + (isLateBinding ? method : method.name) +
10247 "()' instead";
10248 }
10249
10250 if (isLateBinding) {
10251 member = function() {
10252 //<debug warn>
10253 Ext.Logger.deprecate(message, this);
10254 //</debug>
10255
10256 return this[method].apply(this, arguments);
10257 };
10258 }
10259 else {
10260 member = function() {
10261 //<debug warn>
10262 Ext.Logger.deprecate(message, this);
10263 //</debug>
10264
10265 return method.apply(this, arguments);
10266 };
10267 }
10268
10269 if (name in cls.prototype) {
10270 Ext.Object.defineProperty(cls.prototype, name, {
10271 value: null,
10272 writable: true,
10273 configurable: true
10274 });
10275 }
10276
10277 cls.addMember(name, member);
10278 },
10279
10280 //<debug>
10281 /**
10282 * Useful snippet to show an exact, narrowed-down list of top-level Components that are not yet destroyed.
10283 * @private
10284 */
10285 showLeaks: function() {
10286 var map = Ext.ComponentManager.all.map,
10287 leaks = [],
10288 parent;
10289
10290 Ext.Object.each(map, function(id, component) {
10291 while ((parent = component.getParent()) && map.hasOwnProperty(parent.getId())) {
10292 component = parent;
10293 }
10294
10295 if (leaks.indexOf(component) === -1) {
10296 leaks.push(component);
10297 }
10298 });
10299
10300 console.log(leaks);
10301 },
10302 //</debug>
10303
10304 /**
10305 * True when the document is fully initialized and ready for action
10306 * @type Boolean
10307 * @member Ext
10308 * @private
10309 */
10310 isReady : false,
10311
10312 /**
10313 * @private
10314 * @member Ext
10315 */
10316 readyListeners: [],
10317
10318 /**
10319 * @private
10320 * @member Ext
10321 */
10322 triggerReady: function() {
10323 var listeners = Ext.readyListeners,
10324 i, ln, listener;
10325
10326 if (!Ext.isReady) {
10327 Ext.isReady = true;
10328
10329 for (i = 0,ln = listeners.length; i < ln; i++) {
10330 listener = listeners[i];
10331 listener.fn.call(listener.scope);
10332 }
10333 delete Ext.readyListeners;
10334 }
10335 },
10336
10337 /**
10338 * @private
10339 * @member Ext
10340 */
10341 onDocumentReady: function(fn, scope) {
10342 if (Ext.isReady) {
10343 fn.call(scope);
10344 }
10345 else {
10346 var triggerFn = Ext.triggerReady;
10347
10348 Ext.readyListeners.push({
10349 fn: fn,
10350 scope: scope
10351 });
10352
10353 if ((Ext.browser.is.WebWorks || Ext.browser.is.PhoneGap) && !Ext.os.is.Desktop) {
10354 if (!Ext.readyListenerAttached) {
10355 Ext.readyListenerAttached = true;
10356 document.addEventListener(Ext.browser.is.PhoneGap ? 'deviceready' : 'webworksready', triggerFn, false);
10357 }
10358 }
10359 else {
10360 var readyStateRe = (/MSIE 10/.test(navigator.userAgent)) ? /complete|loaded/ : /interactive|complete|loaded/;
10361 if (document.readyState.match(readyStateRe) !== null) {
10362 triggerFn();
10363 }
10364 else if (!Ext.readyListenerAttached) {
10365 Ext.readyListenerAttached = true;
10366 window.addEventListener('DOMContentLoaded', function() {
10367 if (navigator.standalone) {
10368 // When running from Home Screen, the splash screen will not disappear until all
10369 // external resource requests finish.
10370 // The first timeout clears the splash screen
10371 // The second timeout allows inital HTML content to be displayed
10372 setTimeout(function() {
10373 setTimeout(function() {
10374 triggerFn();
10375 }, 1);
10376 }, 1);
10377 }
10378 else {
10379 setTimeout(function() {
10380 triggerFn();
10381 }, 1);
10382 }
10383 }, false);
10384 }
10385 }
10386 }
10387 },
10388
10389 /**
10390 * Calls function after specified delay, or right away when delay == 0.
10391 * @param {Function} callback The callback to execute.
10392 * @param {Object} scope (optional) The scope to execute in.
10393 * @param {Array} args (optional) The arguments to pass to the function.
10394 * @param {Number} delay (optional) Pass a number to delay the call by a number of milliseconds.
10395 * @member Ext
10396 */
10397 callback: function(callback, scope, args, delay) {
10398 if (Ext.isFunction(callback)) {
10399 args = args || [];
10400 scope = scope || window;
10401 if (delay) {
10402 Ext.defer(callback, delay, scope, args);
10403 } else {
10404 callback.apply(scope, args);
10405 }
10406 }
10407 }
10408 });
10409
10410 //<debug>
10411 Ext.Object.defineProperty(Ext, 'Msg', {
10412 get: function() {
10413 Ext.Logger.error("Using Ext.Msg without requiring Ext.MessageBox");
10414 return null;
10415 },
10416 set: function(value) {
10417 Ext.Object.defineProperty(Ext, 'Msg', {
10418 value: value
10419 });
10420 return value;
10421 },
10422 configurable: true
10423 });
10424 //</debug>
10425
10426
10427 //@tag dom,core
10428 //@require Ext-more
10429
10430 /**
10431 * Provides information about browser.
10432 *
10433 * Should not be manually instantiated unless for unit-testing.
10434 * Access the global instance stored in {@link Ext.browser} instead.
10435 * @private
10436 */
10437 Ext.define('Ext.env.Browser', {
10438
10439
10440
10441
10442 statics: {
10443 browserNames: {
10444 ie: 'IE',
10445 firefox: 'Firefox',
10446 safari: 'Safari',
10447 chrome: 'Chrome',
10448 opera: 'Opera',
10449 dolfin: 'Dolfin',
10450 webosbrowser: 'webOSBrowser',
10451 chromeMobile: 'ChromeMobile',
10452 chromeiOS: 'ChromeiOS',
10453 silk: 'Silk',
10454 other: 'Other'
10455 },
10456 engineNames: {
10457 webkit: 'WebKit',
10458 gecko: 'Gecko',
10459 presto: 'Presto',
10460 trident: 'Trident',
10461 other: 'Other'
10462 },
10463 enginePrefixes: {
10464 webkit: 'AppleWebKit/',
10465 gecko: 'Gecko/',
10466 presto: 'Presto/',
10467 trident: 'Trident/'
10468 },
10469 browserPrefixes: {
10470 ie: 'MSIE ',
10471 firefox: 'Firefox/',
10472 chrome: 'Chrome/',
10473 safari: 'Version/',
10474 opera: 'OPR/',
10475 dolfin: 'Dolfin/',
10476 webosbrowser: 'wOSBrowser/',
10477 chromeMobile: 'CrMo/',
10478 chromeiOS: 'CriOS/',
10479 silk: 'Silk/'
10480 }
10481 },
10482
10483 styleDashPrefixes: {
10484 WebKit: '-webkit-',
10485 Gecko: '-moz-',
10486 Trident: '-ms-',
10487 Presto: '-o-',
10488 Other: ''
10489 },
10490
10491 stylePrefixes: {
10492 WebKit: 'Webkit',
10493 Gecko: 'Moz',
10494 Trident: 'ms',
10495 Presto: 'O',
10496 Other: ''
10497 },
10498
10499 propertyPrefixes: {
10500 WebKit: 'webkit',
10501 Gecko: 'moz',
10502 Trident: 'ms',
10503 Presto: 'o',
10504 Other: ''
10505 },
10506
10507 // scope: Ext.env.Browser.prototype
10508
10509 /**
10510 * A "hybrid" property, can be either accessed as a method call, for example:
10511 *
10512 * if (Ext.browser.is('IE')) {
10513 * // ...
10514 * }
10515 *
10516 * Or as an object with Boolean properties, for example:
10517 *
10518 * if (Ext.browser.is.IE) {
10519 * // ...
10520 * }
10521 *
10522 * Versions can be conveniently checked as well. For example:
10523 *
10524 * if (Ext.browser.is.IE6) {
10525 * // Equivalent to (Ext.browser.is.IE && Ext.browser.version.equals(6))
10526 * }
10527 *
10528 * __Note:__ Only {@link Ext.Version#getMajor major component} and {@link Ext.Version#getShortVersion simplified}
10529 * value of the version are available via direct property checking.
10530 *
10531 * Supported values are:
10532 *
10533 * - IE
10534 * - Firefox
10535 * - Safari
10536 * - Chrome
10537 * - Opera
10538 * - WebKit
10539 * - Gecko
10540 * - Presto
10541 * - Trident
10542 * - WebView
10543 * - Other
10544 *
10545 * @param {String} value The OS name to check.
10546 * @return {Boolean}
10547 */
10548 is: Ext.emptyFn,
10549
10550 /**
10551 * The full name of the current browser.
10552 * Possible values are:
10553 *
10554 * - IE
10555 * - Firefox
10556 * - Safari
10557 * - Chrome
10558 * - Opera
10559 * - Other
10560 * @type String
10561 * @readonly
10562 */
10563 name: null,
10564
10565 /**
10566 * Refer to {@link Ext.Version}.
10567 * @type Ext.Version
10568 * @readonly
10569 */
10570 version: null,
10571
10572 /**
10573 * The full name of the current browser's engine.
10574 * Possible values are:
10575 *
10576 * - WebKit
10577 * - Gecko
10578 * - Presto
10579 * - Trident
10580 * - Other
10581 * @type String
10582 * @readonly
10583 */
10584 engineName: null,
10585
10586 /**
10587 * Refer to {@link Ext.Version}.
10588 * @type Ext.Version
10589 * @readonly
10590 */
10591 engineVersion: null,
10592
10593 setFlag: function(name, value) {
10594 if (typeof value == 'undefined') {
10595 value = true;
10596 }
10597
10598 this.is[name] = value;
10599 this.is[name.toLowerCase()] = value;
10600
10601 return this;
10602 },
10603
10604 constructor: function(userAgent) {
10605 /**
10606 * @property {String}
10607 * Browser User Agent string.
10608 */
10609 this.userAgent = userAgent;
10610
10611 var statics = this.statics(),
10612 browserMatch = userAgent.match(new RegExp('((?:' + Ext.Object.getValues(statics.browserPrefixes).join(')|(?:') + '))([\\w\\._]+)')),
10613 engineMatch = userAgent.match(new RegExp('((?:' + Ext.Object.getValues(statics.enginePrefixes).join(')|(?:') + '))([\\w\\._]+)')),
10614 browserNames = statics.browserNames,
10615 browserName = browserNames.other,
10616 engineNames = statics.engineNames,
10617 engineName = engineNames.other,
10618 browserVersion = '',
10619 engineVersion = '',
10620 isWebView = false,
10621 is, i, name;
10622
10623 is = this.is = function(name) {
10624 return is[name] === true;
10625 };
10626
10627 if (browserMatch) {
10628 browserName = browserNames[Ext.Object.getKey(statics.browserPrefixes, browserMatch[1])];
10629 browserVersion = new Ext.Version(browserMatch[2]);
10630 }
10631
10632 if (engineMatch) {
10633 engineName = engineNames[Ext.Object.getKey(statics.enginePrefixes, engineMatch[1])];
10634 engineVersion = new Ext.Version(engineMatch[2]);
10635 }
10636
10637 if (engineName == 'Trident' && browserName != 'IE') {
10638 browserName = 'IE';
10639 var version = userAgent.match(/.*rv:(\d+.\d+)/);
10640 if (version && version.length) {
10641 version = version[1];
10642 browserVersion = new Ext.Version(version);
10643 }
10644 }
10645
10646 // Facebook changes the userAgent when you view a website within their iOS app. For some reason, the strip out information
10647 // about the browser, so we have to detect that and fake it...
10648 if (userAgent.match(/FB/) && browserName == "Other") {
10649 browserName = browserNames.safari;
10650 engineName = engineNames.webkit;
10651 }
10652
10653 if (userAgent.match(/Android.*Chrome/g)) {
10654 browserName = 'ChromeMobile';
10655 }
10656
10657 if (userAgent.match(/OPR/)) {
10658 browserName = 'Opera';
10659 browserMatch = userAgent.match(/OPR\/(\d+.\d+)/);
10660 browserVersion = new Ext.Version(browserMatch[1]);
10661 }
10662
10663 if(browserName === 'Safari' && userAgent.match(/BB10/)) {
10664 browserName = 'BlackBerry';
10665 }
10666
10667 Ext.apply(this, {
10668 engineName: engineName,
10669 engineVersion: engineVersion,
10670 name: browserName,
10671 version: browserVersion
10672 });
10673
10674 this.setFlag(browserName);
10675
10676 if (browserVersion) {
10677 this.setFlag(browserName + (browserVersion.getMajor() || ''));
10678 this.setFlag(browserName + browserVersion.getShortVersion());
10679 }
10680
10681 for (i in browserNames) {
10682 if (browserNames.hasOwnProperty(i)) {
10683 name = browserNames[i];
10684
10685 this.setFlag(name, browserName === name);
10686 }
10687 }
10688
10689 this.setFlag(name);
10690
10691 if (engineVersion) {
10692 this.setFlag(engineName + (engineVersion.getMajor() || ''));
10693 this.setFlag(engineName + engineVersion.getShortVersion());
10694 }
10695
10696 for (i in engineNames) {
10697 if (engineNames.hasOwnProperty(i)) {
10698 name = engineNames[i];
10699
10700 this.setFlag(name, engineName === name);
10701 }
10702 }
10703
10704 this.setFlag('Standalone', !!navigator.standalone);
10705
10706 this.setFlag('Ripple', !!document.getElementById("tinyhippos-injected") && !Ext.isEmpty(window.top.ripple));
10707 this.setFlag('WebWorks', !!window.blackberry);
10708
10709 if (typeof window.PhoneGap != 'undefined' || typeof window.Cordova != 'undefined' || typeof window.cordova != 'undefined') {
10710 isWebView = true;
10711 this.setFlag('PhoneGap');
10712 this.setFlag('Cordova');
10713 }
10714 else if (!!window.isNK) {
10715 isWebView = true;
10716 this.setFlag('Sencha');
10717 }
10718
10719 if (/(Glass)/i.test(userAgent)) {
10720 this.setFlag('GoogleGlass');
10721 }
10722
10723 // Check if running in UIWebView
10724 if (/(iPhone|iPod|iPad).*AppleWebKit(?!.*Safari)(?!.*FBAN)/i.test(userAgent)) {
10725 isWebView = true;
10726 }
10727
10728 // Flag to check if it we are in the WebView
10729 this.setFlag('WebView', isWebView);
10730
10731 /**
10732 * @property {Boolean}
10733 * `true` if browser is using strict mode.
10734 */
10735 this.isStrict = document.compatMode == "CSS1Compat";
10736
10737 /**
10738 * @property {Boolean}
10739 * `true` if page is running over SSL.
10740 */
10741 this.isSecure = /^https/i.test(window.location.protocol);
10742
10743 return this;
10744 },
10745
10746 getStyleDashPrefix: function() {
10747 return this.styleDashPrefixes[this.engineName];
10748 },
10749
10750 getStylePrefix: function() {
10751 return this.stylePrefixes[this.engineName];
10752 },
10753
10754 getVendorProperyName: function(name) {
10755 var prefix = this.propertyPrefixes[this.engineName];
10756
10757 if (prefix.length > 0) {
10758 return prefix + Ext.String.capitalize(name);
10759 }
10760
10761 return name;
10762 },
10763
10764 getPreferredTranslationMethod: function(config) {
10765 if (typeof config == 'object' && 'translationMethod' in config && config.translationMethod !== 'auto') {
10766 return config.translationMethod;
10767 } else {
10768 if (this.is.AndroidStock2 || this.is.IE) {
10769 return 'scrollposition';
10770 }
10771 else {
10772 return 'csstransform';
10773 }
10774 }
10775 }
10776
10777 }, function() {
10778 /**
10779 * @class Ext.browser
10780 * @extends Ext.env.Browser
10781 * @singleton
10782 * Provides useful information about the current browser.
10783 *
10784 * Example:
10785 *
10786 * if (Ext.browser.is.IE) {
10787 * // IE specific code here
10788 * }
10789 *
10790 * if (Ext.browser.is.WebKit) {
10791 * // WebKit specific code here
10792 * }
10793 *
10794 * console.log("Version " + Ext.browser.version);
10795 *
10796 * For a full list of supported values, refer to {@link #is} property/method.
10797 *
10798 * For more information, see the [Environment Detect Guide](../../../core_concepts/environment_detection.html)
10799 */
10800 var browserEnv = Ext.browser = new this(Ext.global.navigator.userAgent);
10801
10802 });
10803
10804 //@tag dom,core
10805 //@require Ext.env.Browser
10806
10807 /**
10808 * Provides information about operating system environment.
10809 *
10810 * Should not be manually instantiated unless for unit-testing.
10811 * Access the global instance stored in {@link Ext.os} instead.
10812 * @private
10813 */
10814 Ext.define('Ext.env.OS', {
10815
10816
10817
10818 statics: {
10819 names: {
10820 ios: 'iOS',
10821 android: 'Android',
10822 windowsPhone: 'WindowsPhone',
10823 webos: 'webOS',
10824 blackberry: 'BlackBerry',
10825 rimTablet: 'RIMTablet',
10826 mac: 'MacOS',
10827 win: 'Windows',
10828 tizen: 'Tizen',
10829 linux: 'Linux',
10830 bada: 'Bada',
10831 chrome: 'ChromeOS',
10832 other: 'Other'
10833 },
10834 prefixes: {
10835 tizen: '(Tizen )',
10836 ios: 'i(?:Pad|Phone|Pod)(?:.*)CPU(?: iPhone)? OS ',
10837 android: '(Android |HTC_|Silk/)', // Some HTC devices ship with an OSX userAgent by default,
10838 // so we need to add a direct check for HTC_
10839 windowsPhone: 'Windows Phone ',
10840 blackberry: '(?:BlackBerry|BB)(?:.*)Version\/',
10841 rimTablet: 'RIM Tablet OS ',
10842 webos: '(?:webOS|hpwOS)\/',
10843 bada: 'Bada\/',
10844 chrome: 'CrOS '
10845 }
10846 },
10847
10848 /**
10849 * A "hybrid" property, can be either accessed as a method call, i.e:
10850 *
10851 * if (Ext.os.is('Android')) {
10852 * // ...
10853 * }
10854 *
10855 * or as an object with boolean properties, i.e:
10856 *
10857 * if (Ext.os.is.Android) {
10858 * // ...
10859 * }
10860 *
10861 * Versions can be conveniently checked as well. For example:
10862 *
10863 * if (Ext.os.is.Android2) {
10864 * // Equivalent to (Ext.os.is.Android && Ext.os.version.equals(2))
10865 * }
10866 *
10867 * if (Ext.os.is.iOS32) {
10868 * // Equivalent to (Ext.os.is.iOS && Ext.os.version.equals(3.2))
10869 * }
10870 *
10871 * Note that only {@link Ext.Version#getMajor major component} and {@link Ext.Version#getShortVersion simplified}
10872 * value of the version are available via direct property checking. Supported values are:
10873 *
10874 * - iOS
10875 * - iPad
10876 * - iPhone
10877 * - iPhone5 (also true for 4in iPods).
10878 * - iPod
10879 * - Android
10880 * - WebOS
10881 * - BlackBerry
10882 * - Bada
10883 * - MacOS
10884 * - Windows
10885 * - Linux
10886 * - Other
10887 * @param {String} value The OS name to check.
10888 * @return {Boolean}
10889 */
10890 is: Ext.emptyFn,
10891
10892 /**
10893 * @property {String} [name=null]
10894 * @readonly
10895 * The full name of the current operating system. Possible values are:
10896 *
10897 * - iOS
10898 * - Android
10899 * - WebOS
10900 * - BlackBerry,
10901 * - MacOS
10902 * - Windows
10903 * - Linux
10904 * - Other
10905 */
10906 name: null,
10907
10908 /**
10909 * @property {Ext.Version} [version=null]
10910 * Refer to {@link Ext.Version}
10911 * @readonly
10912 */
10913 version: null,
10914
10915 setFlag: function(name, value) {
10916 if (typeof value == 'undefined') {
10917 value = true;
10918 }
10919
10920 this.is[name] = value;
10921 this.is[name.toLowerCase()] = value;
10922
10923 return this;
10924 },
10925
10926 constructor: function(userAgent, platform, browserScope) {
10927 var statics = this.statics(),
10928 names = statics.names,
10929 prefixes = statics.prefixes,
10930 name,
10931 version = '',
10932 i, prefix, match, item, is, match1;
10933
10934 browserScope = browserScope || Ext.browser;
10935
10936 is = this.is = function(name) {
10937 return this.is[name] === true;
10938 };
10939
10940 for (i in prefixes) {
10941 if (prefixes.hasOwnProperty(i)) {
10942 prefix = prefixes[i];
10943
10944 match = userAgent.match(new RegExp('(?:'+prefix+')([^\\s;]+)'));
10945
10946 if (match) {
10947 name = names[i];
10948 match1 = match[1];
10949
10950 // This is here because some HTC android devices show an OSX Snow Leopard userAgent by default.
10951 // And the Kindle Fire doesn't have any indicator of Android as the OS in its User Agent
10952 if (match1 && match1 == "HTC_") {
10953 version = new Ext.Version("2.3");
10954 }
10955 else if (match1 && match1 == "Silk/") {
10956 version = new Ext.Version("2.3");
10957 }
10958 else {
10959 version = new Ext.Version(match[match.length - 1]);
10960 }
10961
10962 break;
10963 }
10964 }
10965 }
10966
10967 if (!name) {
10968 name = names[(userAgent.toLowerCase().match(/mac|win|linux/) || ['other'])[0]];
10969 version = new Ext.Version('');
10970 }
10971
10972 this.name = name;
10973 this.version = version;
10974
10975 if (platform) {
10976 this.setFlag(platform.replace(/ simulator$/i, ''));
10977 }
10978
10979 this.setFlag(name);
10980
10981 if (version) {
10982 this.setFlag(name + (version.getMajor() || ''));
10983 this.setFlag(name + version.getShortVersion());
10984 }
10985
10986 for (i in names) {
10987 if (names.hasOwnProperty(i)) {
10988 item = names[i];
10989
10990 if (!is.hasOwnProperty(name)) {
10991 this.setFlag(item, (name === item));
10992 }
10993 }
10994 }
10995
10996 // Detect if the device is the iPhone 5.
10997 if (this.name == "iOS" && window.screen.height == 568) {
10998 this.setFlag('iPhone5');
10999 }
11000
11001
11002 if (browserScope.is.Safari || browserScope.is.Silk) {
11003 // Ext.browser.version.shortVersion == 501 is for debugging off device
11004 if (this.is.Android2 || this.is.Android3 || browserScope.version.shortVersion == 501) {
11005 browserScope.setFlag("AndroidStock");
11006 browserScope.setFlag("AndroidStock2");
11007 }
11008 if (this.is.Android4) {
11009 browserScope.setFlag("AndroidStock");
11010 browserScope.setFlag("AndroidStock4");
11011 }
11012 }
11013
11014 return this;
11015 }
11016
11017 }, function() {
11018
11019 var navigation = Ext.global.navigator,
11020 userAgent = navigation.userAgent,
11021 osEnv, osName, deviceType;
11022
11023
11024 /**
11025 * @class Ext.os
11026 * @extends Ext.env.OS
11027 * @singleton
11028 * Provides useful information about the current operating system environment.
11029 *
11030 * Example:
11031 *
11032 * if (Ext.os.is.Windows) {
11033 * // Windows specific code here
11034 * }
11035 *
11036 * if (Ext.os.is.iOS) {
11037 * // iPad, iPod, iPhone, etc.
11038 * }
11039 *
11040 * console.log("Version " + Ext.os.version);
11041 *
11042 * For a full list of supported values, refer to the {@link #is} property/method.
11043 *
11044 * For more information, see the [Environment Detect Guide](../../../core_concepts/environment_detection.html)
11045 */
11046 Ext.os = osEnv = new this(userAgent, navigation.platform);
11047
11048 osName = osEnv.name;
11049
11050 var search = window.location.search.match(/deviceType=(Tablet|Phone)/),
11051 nativeDeviceType = window.deviceType;
11052
11053 // Override deviceType by adding a get variable of deviceType. NEEDED FOR DOCS APP.
11054 // E.g: example/kitchen-sink.html?deviceType=Phone
11055 if (search && search[1]) {
11056 deviceType = search[1];
11057 }
11058 else if (nativeDeviceType === 'iPhone') {
11059 deviceType = 'Phone';
11060 }
11061 else if (nativeDeviceType === 'iPad') {
11062 deviceType = 'Tablet';
11063 }
11064 else {
11065 if (!osEnv.is.Android && !osEnv.is.iOS && !osEnv.is.WindowsPhone && /Windows|Linux|MacOS/.test(osName)) {
11066 deviceType = 'Desktop';
11067
11068 // always set it to false when you are on a desktop not using Ripple Emulation
11069 Ext.browser.is.WebView = Ext.browser.is.Ripple ? true : false;
11070 }
11071 else if (osEnv.is.iPad || osEnv.is.RIMTablet || osEnv.is.Android3 || Ext.browser.is.Silk || (osEnv.is.Android && userAgent.search(/mobile/i) == -1)) {
11072 deviceType = 'Tablet';
11073 }
11074 else {
11075 deviceType = 'Phone';
11076 }
11077 }
11078
11079 /**
11080 * @property {String} deviceType
11081 * The generic type of the current device.
11082 *
11083 * Possible values:
11084 *
11085 * - Phone
11086 * - Tablet
11087 * - Desktop
11088 *
11089 * For testing purposes the deviceType can be overridden by adding
11090 * a deviceType parameter to the URL of the page, like so:
11091 *
11092 * http://localhost/mypage.html?deviceType=Tablet
11093 *
11094 */
11095 osEnv.setFlag(deviceType, true);
11096 osEnv.deviceType = deviceType;
11097
11098
11099 /**
11100 * @class Ext.is
11101 * Used to detect if the current browser supports a certain feature, and the type of the current browser.
11102 * @deprecated 2.0.0
11103 * Please refer to the {@link Ext.browser}, {@link Ext.os} and {@link Ext.feature} classes instead.
11104 */
11105 });
11106
11107 //@tag dom,core
11108
11109 /**
11110 * Provides information about browser.
11111 *
11112 * Should not be manually instantiated unless for unit-testing.
11113 * Access the global instance stored in {@link Ext.browser} instead.
11114 * @private
11115 */
11116 Ext.define('Ext.env.Feature', {
11117
11118
11119
11120 constructor: function() {
11121 this.testElements = {};
11122
11123 this.has = function(name) {
11124 return !!this.has[name];
11125 };
11126
11127 if (!Ext.theme) {
11128 Ext.theme = {
11129 name: 'Default'
11130 };
11131 }
11132
11133 Ext.theme.is = {};
11134 Ext.theme.is[Ext.theme.name] = true;
11135
11136 Ext.onDocumentReady(function() {
11137 this.registerTest({
11138 ProperHBoxStretching: function() {
11139 // IE10 currently has a bug in their flexbox row layout. We feature detect the issue here.
11140 var bodyElement = document.createElement('div'),
11141 innerElement = bodyElement.appendChild(document.createElement('div')),
11142 contentElement = innerElement.appendChild(document.createElement('div')),
11143 innerWidth;
11144
11145 bodyElement.setAttribute('style', 'width: 100px; height: 100px; position: relative;');
11146 innerElement.setAttribute('style', 'position: absolute; display: -ms-flexbox; display: -webkit-flex; display: -moz-flexbox; display: flex; -ms-flex-direction: row; -webkit-flex-direction: row; -moz-flex-direction: row; flex-direction: row; min-width: 100%;');
11147 contentElement.setAttribute('style', 'width: 200px; height: 50px;');
11148 document.body.appendChild(bodyElement);
11149 innerWidth = innerElement.offsetWidth;
11150 document.body.removeChild(bodyElement);
11151
11152 return (innerWidth > 100);
11153 }
11154 });
11155 }, this);
11156 },
11157
11158 getTestElement: function(tag, createNew) {
11159 if (tag === undefined) {
11160 tag = 'div';
11161 }
11162 else if (typeof tag !== 'string') {
11163 return tag;
11164 }
11165
11166 if (createNew) {
11167 return document.createElement(tag);
11168 }
11169
11170 if (!this.testElements[tag]) {
11171 this.testElements[tag] = document.createElement(tag);
11172 }
11173
11174 return this.testElements[tag];
11175 },
11176
11177 isStyleSupported: function(name, tag) {
11178 var elementStyle = this.getTestElement(tag).style,
11179 cName = Ext.String.capitalize(name);
11180
11181 if (typeof elementStyle[name] !== 'undefined'
11182 || typeof elementStyle[Ext.browser.getStylePrefix(name) + cName] !== 'undefined') {
11183 return true;
11184 }
11185
11186 return false;
11187 },
11188
11189 isStyleSupportedWithoutPrefix: function(name, tag) {
11190 var elementStyle = this.getTestElement(tag).style;
11191
11192 if (typeof elementStyle[name] !== 'undefined') {
11193 return true;
11194 }
11195
11196 return false;
11197 },
11198
11199 isEventSupported: function(name, tag) {
11200 if (tag === undefined) {
11201 tag = window;
11202 }
11203
11204 var element = this.getTestElement(tag),
11205 eventName = 'on' + name.toLowerCase(),
11206 isSupported = (eventName in element);
11207
11208 if (!isSupported) {
11209 if (element.setAttribute && element.removeAttribute) {
11210 element.setAttribute(eventName, '');
11211 isSupported = typeof element[eventName] === 'function';
11212
11213 if (typeof element[eventName] !== 'undefined') {
11214 element[eventName] = undefined;
11215 }
11216
11217 element.removeAttribute(eventName);
11218 }
11219 }
11220
11221 return isSupported;
11222 },
11223
11224 getSupportedPropertyName: function(object, name) {
11225 var vendorName = Ext.browser.getVendorProperyName(name);
11226
11227 if (vendorName in object) {
11228 return vendorName;
11229 }
11230 else if (name in object) {
11231 return name;
11232 }
11233
11234 return null;
11235 },
11236
11237 registerTest: Ext.Function.flexSetter(function(name, fn) {
11238 this.has[name] = fn.call(this);
11239
11240 return this;
11241 })
11242
11243 }, function() {
11244
11245 /**
11246 * @class Ext.feature
11247 * @extend Ext.env.Feature
11248 * @singleton
11249 *
11250 * A simple class to verify if a browser feature exists or not on the current device.
11251 *
11252 * if (Ext.feature.has.Canvas) {
11253 * // do some cool things with canvas here
11254 * }
11255 *
11256 * See the {@link #has} property/method for details of the features that can be detected.
11257 *
11258 * For more information, see the [Environment Detect Guide](../../../core_concepts/environment_detection.html)
11259 */
11260 Ext.feature = new this;
11261
11262 var has = Ext.feature.has;
11263
11264 /**
11265 * @method has
11266 * @member Ext.feature
11267 * Verifies if a browser feature exists or not on the current device.
11268 *
11269 * A "hybrid" property, can be either accessed as a method call, i.e:
11270 *
11271 * if (Ext.feature.has('Canvas')) {
11272 * // ...
11273 * }
11274 *
11275 * or as an object with boolean properties, i.e:
11276 *
11277 * if (Ext.feature.has.Canvas) {
11278 * // ...
11279 * }
11280 *
11281 * Possible properties/parameter values:
11282 *
11283 * - Canvas
11284 * - Svg
11285 * - Vml
11286 * - Touch - supports touch events (`touchstart`).
11287 * - Orientation - supports different orientations.
11288 * - OrientationChange - supports the `orientationchange` event.
11289 * - DeviceMotion - supports the `devicemotion` event.
11290 * - Geolocation
11291 * - SqlDatabase
11292 * - WebSockets
11293 * - Range - supports [DOM document fragments.][1]
11294 * - CreateContextualFragment - supports HTML fragment parsing using [range.createContextualFragment()][2].
11295 * - History - supports history management with [history.pushState()][3].
11296 * - CssTransforms
11297 * - Css3dTransforms
11298 * - CssAnimations
11299 * - CssTransitions
11300 * - Audio - supports the `<audio>` tag.
11301 * - Video - supports the `<video>` tag.
11302 * - ClassList - supports the HTML5 classList API.
11303 * - LocalStorage - LocalStorage is supported and can be written to.
11304 * - NumericInputPlaceHolder - Supports placeholders on numeric input fields
11305 * - XHR2 - Supports XMLHttpRequest
11306 * - XHRUploadProgress - Supports XMLHttpRequest upload progress info
11307 *
11308 * [1]: https://developer.mozilla.org/en/DOM/range
11309 * [2]: https://developer.mozilla.org/en/DOM/range.createContextualFragment
11310 * [3]: https://developer.mozilla.org/en/DOM/Manipulating_the_browser_history#The_pushState().C2.A0method
11311 *
11312 * @param {String} value The feature name to check.
11313 * @return {Boolean}
11314 */
11315 Ext.feature.registerTest({
11316 Canvas: function() {
11317 var element = this.getTestElement('canvas');
11318 return !!(element && element.getContext && element.getContext('2d'));
11319 },
11320
11321 Svg: function() {
11322 var doc = document;
11323
11324 return !!(doc.createElementNS && !!doc.createElementNS("http:/" + "/www.w3.org/2000/svg", "svg").createSVGRect);
11325 },
11326
11327 Vml: function() {
11328 var element = this.getTestElement(),
11329 ret = false;
11330
11331 element.innerHTML = "<!--[if vml]><br><![endif]-->";
11332 ret = (element.childNodes.length === 1);
11333 element.innerHTML = "";
11334
11335 return ret;
11336 },
11337
11338 Touch: function() {
11339 return Ext.browser.is.Ripple || (this.isEventSupported('touchstart') && !(Ext.os && Ext.os.name.match(/Windows|MacOS|Linux/) && !Ext.os.is.BlackBerry6));
11340 },
11341
11342 Pointer: function() {
11343 return !!window.navigator.msPointerEnabled;
11344 },
11345
11346 Orientation: function() {
11347 return 'orientation' in window;
11348 },
11349
11350 OrientationChange: function() {
11351 return this.isEventSupported('orientationchange');
11352 },
11353
11354 DeviceMotion: function() {
11355 return this.isEventSupported('devicemotion');
11356 },
11357
11358 Geolocation: function() {
11359 return 'geolocation' in window.navigator;
11360 },
11361
11362 SqlDatabase: function() {
11363 return 'openDatabase' in window;
11364 },
11365
11366 WebSockets: function() {
11367 return 'WebSocket' in window;
11368 },
11369
11370 Range: function() {
11371 return !!document.createRange;
11372 },
11373
11374 CreateContextualFragment: function() {
11375 var range = !!document.createRange ? document.createRange() : false;
11376 return range && !!range.createContextualFragment;
11377 },
11378
11379 History: function() {
11380 return ('history' in window && 'pushState' in window.history);
11381 },
11382
11383 CssTransforms: function() {
11384 return this.isStyleSupported('transform');
11385 },
11386
11387 CssTransformNoPrefix: function() {
11388 // This extra check is needed to get around a browser bug where both 'transform' and '-webkit-transform' are present
11389 // but the device really only uses '-webkit-transform'. This is seen on the HTC One for example.
11390 // https://sencha.jira.com/browse/TOUCH-5029
11391 if(!Ext.browser.is.AndroidStock) {
11392 return this.isStyleSupportedWithoutPrefix('transform')
11393 } else {
11394 return this.isStyleSupportedWithoutPrefix('transform') && !this.isStyleSupportedWithoutPrefix('-webkit-transform');
11395 }
11396 },
11397
11398 Css3dTransforms: function() {
11399 // See https://sencha.jira.com/browse/TOUCH-1544
11400 return this.has('CssTransforms') && this.isStyleSupported('perspective') && !Ext.browser.is.AndroidStock2;
11401 },
11402
11403 CssAnimations: function() {
11404 return this.isStyleSupported('animationName');
11405 },
11406
11407 CssTransitions: function() {
11408 return this.isStyleSupported('transitionProperty');
11409 },
11410
11411 Audio: function() {
11412 return !!this.getTestElement('audio').canPlayType;
11413 },
11414
11415 Video: function() {
11416 return !!this.getTestElement('video').canPlayType;
11417 },
11418
11419 ClassList: function() {
11420 return "classList" in this.getTestElement();
11421 },
11422
11423 LocalStorage : function() {
11424 var supported = false;
11425
11426 try {
11427 if ('localStorage' in window && window['localStorage'] !== null) {
11428 //this should throw an error in private browsing mode in iOS
11429 localStorage.setItem('sencha-localstorage-test', 'test success');
11430 //clean up if setItem worked
11431 localStorage.removeItem('sencha-localstorage-test');
11432 supported = true;
11433 }
11434 } catch ( e ) {}
11435
11436 return supported;
11437 },
11438
11439 MatchMedia: function() {
11440 return "matchMedia" in window;
11441 },
11442
11443 XHR2 : function() {
11444 return window.ProgressEvent && window.FormData && window.XMLHttpRequest && ('withCredentials' in new XMLHttpRequest);
11445 },
11446
11447 XHRUploadProgress : function() {
11448 if(window.XMLHttpRequest && !Ext.browser.is.AndroidStock) {
11449 var xhr = new XMLHttpRequest();
11450 return xhr && ('upload' in xhr) && ('onprogress' in xhr.upload);
11451 }
11452 return false;
11453 },
11454
11455 NumericInputPlaceHolder: function() {
11456 return !(Ext.browser.is.AndroidStock4 && Ext.os.version.getMinor() < 2);
11457 }
11458 });
11459
11460 });
11461
11462 //@tag dom,core
11463 //@define Ext.DomQuery
11464 //@define Ext.core.DomQuery
11465 //@require Ext.env.Feature
11466
11467 /**
11468 * @class Ext.DomQuery
11469 * @alternateClassName Ext.core.DomQuery
11470 * @extend Ext.dom.Query
11471 * @singleton
11472 *
11473 * Provides functionality to select elements on the page based on a CSS selector. Delegates to
11474 * document.querySelectorAll. More information can be found at
11475 * [http://www.w3.org/TR/css3-selectors/](http://www.w3.org/TR/css3-selectors/)
11476 *
11477 * All selectors, attribute filters and pseudos below can be combined infinitely in any order. For example
11478 * `div.foo:nth-child(odd)[@foo=bar].bar:first` would be a perfectly valid selector.
11479 *
11480 * ## Element Selectors:
11481 *
11482 * * \* any element
11483 * * E an element with the tag E
11484 * * E F All descendant elements of E that have the tag F
11485 * * E > F or E/F all direct children elements of E that have the tag F
11486 * * E + F all elements with the tag F that are immediately preceded by an element with the tag E
11487 * * E ~ F all elements with the tag F that are preceded by a sibling element with the tag E
11488 *
11489 * ## Attribute Selectors:
11490 *
11491 * The use of @ and quotes are optional. For example, div[@foo='bar'] is also a valid attribute selector.
11492 *
11493 * * E[foo] has an attribute "foo"
11494 * * E[foo=bar] has an attribute "foo" that equals "bar"
11495 * * E[foo^=bar] has an attribute "foo" that starts with "bar"
11496 * * E[foo$=bar] has an attribute "foo" that ends with "bar"
11497 * * E[foo*=bar] has an attribute "foo" that contains the substring "bar"
11498 * * E[foo%=2] has an attribute "foo" that is evenly divisible by 2
11499 * * E[foo!=bar] has an attribute "foo" that does not equal "bar"
11500 *
11501 * ## Pseudo Classes:
11502 *
11503 * * E:first-child E is the first child of its parent
11504 * * E:last-child E is the last child of its parent
11505 * * E:nth-child(n) E is the nth child of its parent (1 based as per the spec)
11506 * * E:nth-child(odd) E is an odd child of its parent
11507 * * E:nth-child(even) E is an even child of its parent
11508 * * E:only-child E is the only child of its parent
11509 * * E:checked E is an element that is has a checked attribute that is true (e.g. a radio or checkbox)
11510 * * E:first the first E in the resultset
11511 * * E:last the last E in the resultset
11512 * * E:nth(n) the nth E in the resultset (1 based)
11513 * * E:odd shortcut for :nth-child(odd)
11514 * * E:even shortcut for :nth-child(even)
11515 * * E:not(S) an E element that does not match simple selector S
11516 * * E:has(S) an E element that has a descendant that matches simple selector S
11517 * * E:next(S) an E element whose next sibling matches simple selector S
11518 * * E:prev(S) an E element whose previous sibling matches simple selector S
11519 * * E:any(S1|S2|S2) an E element which matches any of the simple selectors S1, S2 or S3//\\
11520 *
11521 * ## CSS Value Selectors:
11522 *
11523 * * E{display=none} CSS value "display" that equals "none"
11524 * * E{display^=none} CSS value "display" that starts with "none"
11525 * * E{display$=none} CSS value "display" that ends with "none"
11526 * * E{display*=none} CSS value "display" that contains the substring "none"
11527 * * E{display%=2} CSS value "display" that is evenly divisible by 2
11528 * * E{display!=none} CSS value "display" that does not equal "none"
11529 */
11530
11531 /**
11532 * See {@link Ext.DomQuery} which is the singleton instance of this
11533 * class. You most likely don't need to instantiate Ext.dom.Query by
11534 * yourself.
11535 * @private
11536 */
11537 Ext.define('Ext.dom.Query', {
11538 /**
11539 * Selects a group of elements.
11540 * @param {String} selector The selector/xpath query (can be a comma separated list of selectors)
11541 * @param {HTMLElement/String} [root] The start of the query (defaults to document).
11542 * @return {HTMLElement[]} An Array of DOM elements which match the selector. If there are
11543 * no matches, and empty Array is returned.
11544 */
11545 select: function(q, root) {
11546 var results = [],
11547 nodes, i, j, qlen, nlen;
11548
11549 root = root || document;
11550
11551 if (typeof root == 'string') {
11552 root = document.getElementById(root);
11553 }
11554
11555 q = q.split(",");
11556
11557 for (i = 0,qlen = q.length; i < qlen; i++) {
11558 if (typeof q[i] == 'string') {
11559
11560 //support for node attribute selection
11561 if (q[i][0] == '@') {
11562 nodes = root.getAttributeNode(q[i].substring(1));
11563 results.push(nodes);
11564 }
11565 else {
11566 nodes = root.querySelectorAll(q[i]);
11567
11568 for (j = 0,nlen = nodes.length; j < nlen; j++) {
11569 results.push(nodes[j]);
11570 }
11571 }
11572 }
11573 }
11574
11575 return results;
11576 },
11577
11578 /**
11579 * Selects a single element.
11580 * @param {String} selector The selector/xpath query
11581 * @param {HTMLElement/String} [root] The start of the query (defaults to document).
11582 * @return {HTMLElement} The DOM element which matched the selector.
11583 */
11584 selectNode: function(q, root) {
11585 return this.select(q, root)[0];
11586 },
11587
11588 /**
11589 * Returns true if the passed element(s) match the passed simple selector (e.g. div.some-class or span:first-child)
11590 * @param {String/HTMLElement/Array} el An element id, element or array of elements
11591 * @param {String} selector The simple selector to test
11592 * @return {Boolean}
11593 */
11594 is: function (el, q) {
11595 var root, is, i, ln;
11596
11597 if (typeof el == "string") {
11598 el = document.getElementById(el);
11599 }
11600
11601 if (Ext.isArray(el)) {
11602 is = true;
11603 ln = el.length;
11604 for (i = 0; i < ln; i++) {
11605 if (!this.is(el[i], q)) {
11606 is = false;
11607 break;
11608 }
11609 }
11610 }
11611 else {
11612 root = el.parentNode;
11613
11614 if (!root) {
11615 root = document.createDocumentFragment();
11616 root.appendChild(el);
11617 is = this.select(q, root).indexOf(el) !== -1;
11618 root.removeChild(el);
11619 root = null;
11620 } else {
11621 is = this.select(q, root).indexOf(el) !== -1;
11622 }
11623 }
11624 return is;
11625 },
11626
11627 isXml: function(el) {
11628 var docEl = (el ? el.ownerDocument || el : 0).documentElement;
11629 return docEl ? docEl.nodeName !== "HTML" : false;
11630 }
11631
11632 }, function() {
11633 Ext.ns('Ext.core');
11634 Ext.core.DomQuery = Ext.DomQuery = new this();
11635 /**
11636 * Shorthand of {@link Ext.dom.Query#select}
11637 * @member Ext
11638 * @method query
11639 * @inheritdoc Ext.dom.Query#select
11640 */
11641 Ext.query = Ext.Function.alias(Ext.DomQuery, 'select');
11642 });
11643
11644 //@tag dom,core
11645 //@define Ext.DomHelper
11646 //@require Ext.dom.Query
11647
11648 /**
11649 * @class Ext.DomHelper
11650 * @alternateClassName Ext.dom.Helper
11651 * @singleton
11652 *
11653 * The DomHelper class provides a layer of abstraction from DOM and transparently supports creating elements via DOM or
11654 * using HTML fragments. It also has the ability to create HTML fragment templates from your DOM building code.
11655 *
11656 * ## DomHelper element specification object
11657 *
11658 * A specification object is used when creating elements. Attributes of this object are assumed to be element
11659 * attributes, except for 4 special attributes:
11660 *
11661 * * **tag**: The tag name of the element
11662 * * **children (or cn)**: An array of the same kind of element definition objects to be created and appended. These
11663 * can be nested as deep as you want.
11664 * * **cls**: The class attribute of the element. This will end up being either the "class" attribute on a HTML
11665 * fragment or className for a DOM node, depending on whether DomHelper is using fragments or DOM.
11666 * * **html**: The innerHTML for the element
11667 *
11668 * ## Insertion methods
11669 *
11670 * Commonly used insertion methods:
11671 *
11672 * * {@link #append}
11673 * * {@link #insertBefore}
11674 * * {@link #insertAfter}
11675 * * {@link #overwrite}
11676 * * {@link #insertHtml}
11677 *
11678 * ## Example
11679 *
11680 * This is an example, where an unordered list with 3 children items is appended to an existing element with id
11681 * 'my-div':
11682 *
11683 * var dh = Ext.DomHelper; // create shorthand alias
11684 * // specification object
11685 * var spec = {
11686 * id: 'my-ul',
11687 * tag: 'ul',
11688 * cls: 'my-list',
11689 * // append children after creating
11690 * children: [ // may also specify 'cn' instead of 'children'
11691 * {tag: 'li', id: 'item0', html: 'List Item 0'},
11692 * {tag: 'li', id: 'item1', html: 'List Item 1'},
11693 * {tag: 'li', id: 'item2', html: 'List Item 2'}
11694 * ]
11695 * };
11696 * var list = dh.append(
11697 * 'my-div', // the context element 'my-div' can either be the id or the actual node
11698 * spec // the specification object
11699 * );
11700 *
11701 * Element creation specification parameters in this class may also be passed as an Array of specification objects.
11702 * This can be used to insert multiple sibling nodes into an existing container very efficiently. For example, to add
11703 * more list items to the example above:
11704 *
11705 * dh.append('my-ul', [
11706 * {tag: 'li', id: 'item3', html: 'List Item 3'},
11707 * {tag: 'li', id: 'item4', html: 'List Item 4'}
11708 * ]);
11709 *
11710 * ## Templating
11711 *
11712 * The real power is in the built-in templating. Instead of creating or appending any elements, createTemplate returns
11713 * a Template object which can be used over and over to insert new elements. Revisiting the example above, we could
11714 * utilize templating this time:
11715 *
11716 * // create the node
11717 * var list = dh.append('my-div', {tag: 'ul', cls: 'my-list'});
11718 * // get template
11719 * var tpl = dh.createTemplate({tag: 'li', id: 'item{0}', html: 'List Item {0}'});
11720 *
11721 * for(var i = 0; i < 5; i++){
11722 * tpl.append(list, i); // use template to append to the actual node
11723 * }
11724 *
11725 * An example using a template:
11726 *
11727 * var html = '"{0}" href="{1}" class="nav">{2}';
11728 *
11729 * var tpl = new Ext.DomHelper.createTemplate(html);
11730 * tpl.append('blog-roll', ['link1', 'http://www.tommymaintz.com/', "Tommy's Site"]);
11731 * tpl.append('blog-roll', ['link2', 'http://www.avins.org/', "Jamie's Site"]);
11732 *
11733 * The same example using named parameters:
11734 *
11735 * var html = '"{id}" href="{url}" class="nav">{text}';
11736 *
11737 * var tpl = new Ext.DomHelper.createTemplate(html);
11738 * tpl.append('blog-roll', {
11739 * id: 'link1',
11740 * url: 'http://www.tommymaintz.com/',
11741 * text: "Tommy's Site"
11742 * });
11743 * tpl.append('blog-roll', {
11744 * id: 'link2',
11745 * url: 'http://www.avins.org/',
11746 * text: "Jamie's Site"
11747 * });
11748 *
11749 * ## Compiling Templates
11750 *
11751 * Templates are applied using regular expressions. The performance is great, but if you are adding a bunch of DOM
11752 * elements using the same template, you can increase performance even further by "compiling" the template. The way
11753 * "compile()" works is the template is parsed and broken up at the different variable points and a dynamic function is
11754 * created and eval'ed. The generated function performs string concatenation of these parts and the passed variables
11755 * instead of using regular expressions.
11756 *
11757 * var html = '"{id}" href="{url}" class="nav">{text}';
11758 *
11759 * var tpl = new Ext.DomHelper.createTemplate(html);
11760 * tpl.compile();
11761 *
11762 * // ... use template like normal
11763 *
11764 * ## Performance Boost
11765 *
11766 * DomHelper will transparently create HTML fragments when it can. Using HTML fragments instead of DOM can
11767 * significantly boost performance.
11768 *
11769 * Element creation specification parameters may also be strings. If useDom is false, then the string is used as
11770 * innerHTML. If useDom is true, a string specification results in the creation of a text node. Usage:
11771 *
11772 * Ext.DomHelper.useDom = true; // force it to use DOM; reduces performance
11773 *
11774 */
11775 Ext.define('Ext.dom.Helper', {
11776 emptyTags : /^(?:br|frame|hr|img|input|link|meta|range|spacer|wbr|area|param|col)$/i,
11777 confRe : /tag|children|cn|html|tpl|tplData$/i,
11778 endRe : /end/i,
11779
11780 attribXlat: { cls : 'class', htmlFor : 'for' },
11781
11782 closeTags: {},
11783
11784 decamelizeName : function () {
11785 var camelCaseRe = /([a-z])([A-Z])/g,
11786 cache = {};
11787
11788 function decamel (match, p1, p2) {
11789 return p1 + '-' + p2.toLowerCase();
11790 }
11791
11792 return function (s) {
11793 return cache[s] || (cache[s] = s.replace(camelCaseRe, decamel));
11794 };
11795 }(),
11796
11797 generateMarkup: function(spec, buffer) {
11798 var me = this,
11799 attr, val, tag, i, closeTags;
11800
11801 if (typeof spec == "string") {
11802 buffer.push(spec);
11803 } else if (Ext.isArray(spec)) {
11804 for (i = 0; i < spec.length; i++) {
11805 if (spec[i]) {
11806 me.generateMarkup(spec[i], buffer);
11807 }
11808 }
11809 } else {
11810 tag = spec.tag || 'div';
11811 buffer.push('<', tag);
11812
11813 for (attr in spec) {
11814 if (spec.hasOwnProperty(attr)) {
11815 val = spec[attr];
11816 if (!me.confRe.test(attr)) {
11817 if (typeof val == "object") {
11818 buffer.push(' ', attr, '="');
11819 me.generateStyles(val, buffer).push('"');
11820 } else {
11821 buffer.push(' ', me.attribXlat[attr] || attr, '="', val, '"');
11822 }
11823 }
11824 }
11825 }
11826
11827 // Now either just close the tag or try to add children and close the tag.
11828 if (me.emptyTags.test(tag)) {
11829 buffer.push('/>');
11830 } else {
11831 buffer.push('>');
11832
11833 // Apply the tpl html, and cn specifications
11834 if ((val = spec.tpl)) {
11835 val.applyOut(spec.tplData, buffer);
11836 }
11837 if ((val = spec.html)) {
11838 buffer.push(val);
11839 }
11840 if ((val = spec.cn || spec.children)) {
11841 me.generateMarkup(val, buffer);
11842 }
11843
11844 // we generate a lot of close tags, so cache them rather than push 3 parts
11845 closeTags = me.closeTags;
11846 buffer.push(closeTags[tag] || (closeTags[tag] = '</' + tag + '>'));
11847 }
11848 }
11849
11850 return buffer;
11851 },
11852
11853 /**
11854 * Converts the styles from the given object to text. The styles are CSS style names
11855 * with their associated value.
11856 *
11857 * The basic form of this method returns a string:
11858 *
11859 * var s = Ext.DomHelper.generateStyles({
11860 * backgroundColor: 'red'
11861 * });
11862 *
11863 * // s = 'background-color:red;'
11864 *
11865 * Alternatively, this method can append to an output array.
11866 *
11867 * var buf = [];
11868 *
11869 * // ...
11870 *
11871 * Ext.DomHelper.generateStyles({
11872 * backgroundColor: 'red'
11873 * }, buf);
11874 *
11875 * In this case, the style text is pushed on to the array and the array is returned.
11876 *
11877 * @param {Object} styles The object describing the styles.
11878 * @param {String[]} [buffer] The output buffer.
11879 * @return {String/String[]} If buffer is passed, it is returned. Otherwise the style
11880 * string is returned.
11881 */
11882 generateStyles: function (styles, buffer) {
11883 var a = buffer || [],
11884 name;
11885
11886 for (name in styles) {
11887 if (styles.hasOwnProperty(name)) {
11888 a.push(this.decamelizeName(name), ':', styles[name], ';');
11889 }
11890 }
11891
11892 return buffer || a.join('');
11893 },
11894
11895 /**
11896 * Returns the markup for the passed Element(s) config.
11897 * @param {Object} spec The DOM object spec (and children).
11898 * @return {String}
11899 */
11900 markup: function(spec) {
11901 if (typeof spec == "string") {
11902 return spec;
11903 }
11904
11905 var buf = this.generateMarkup(spec, []);
11906 return buf.join('');
11907 },
11908
11909 /**
11910 * Applies a style specification to an element.
11911 * @param {String/HTMLElement} el The element to apply styles to
11912 * @param {String/Object/Function} styles A style specification string e.g. 'width:100px', or object in the form {width:'100px'}, or
11913 * a function which returns such a specification.
11914 */
11915 applyStyles: function(el, styles) {
11916 Ext.fly(el).applyStyles(styles);
11917 },
11918
11919 /**
11920 * @private
11921 * Fix for browsers which no longer support createContextualFragment
11922 */
11923 createContextualFragment: function(html){
11924 var div = document.createElement("div"),
11925 fragment = document.createDocumentFragment(),
11926 i = 0,
11927 length, childNodes;
11928
11929 div.innerHTML = html;
11930 childNodes = div.childNodes;
11931 length = childNodes.length;
11932
11933 for (; i < length; i++) {
11934 fragment.appendChild(childNodes[i].cloneNode(true));
11935 }
11936
11937 return fragment;
11938 },
11939
11940 /**
11941 * Inserts an HTML fragment into the DOM.
11942 * @param {String} where Where to insert the html in relation to el - beforeBegin, afterBegin, beforeEnd, afterEnd.
11943 *
11944 * For example take the following HTML: `<div>Contents</div>`
11945 *
11946 * Using different `where` values inserts element to the following places:
11947 *
11948 * - beforeBegin: `<HERE><div>Contents</div>`
11949 * - afterBegin: `<div><HERE>Contents</div>`
11950 * - beforeEnd: `<div>Contents<HERE></div>`
11951 * - afterEnd: `<div>Contents</div><HERE>`
11952 *
11953 * @param {HTMLElement/TextNode} el The context element
11954 * @param {String} html The HTML fragment
11955 * @return {HTMLElement} The new node
11956 */
11957 insertHtml: function(where, el, html) {
11958 var setStart, range, frag, rangeEl, isBeforeBegin, isAfterBegin;
11959
11960 where = where.toLowerCase();
11961
11962 if (Ext.isTextNode(el)) {
11963 if (where == 'afterbegin' ) {
11964 where = 'beforebegin';
11965 }
11966 else if (where == 'beforeend') {
11967 where = 'afterend';
11968 }
11969 }
11970
11971 isBeforeBegin = where == 'beforebegin';
11972 isAfterBegin = where == 'afterbegin';
11973
11974 range = Ext.feature.has.CreateContextualFragment ? el.ownerDocument.createRange() : undefined;
11975 setStart = 'setStart' + (this.endRe.test(where) ? 'After' : 'Before');
11976
11977 if (isBeforeBegin || where == 'afterend') {
11978 if (range) {
11979 range[setStart](el);
11980 frag = range.createContextualFragment(html);
11981 }
11982 else {
11983 frag = this.createContextualFragment(html);
11984 }
11985 el.parentNode.insertBefore(frag, isBeforeBegin ? el : el.nextSibling);
11986 return el[(isBeforeBegin ? 'previous' : 'next') + 'Sibling'];
11987 }
11988 else {
11989 rangeEl = (isAfterBegin ? 'first' : 'last') + 'Child';
11990 if (el.firstChild) {
11991 if (range) {
11992 // Creating ranges on a hidden element throws an error, checking for the element being painted is
11993 // VERY expensive, so we'll catch the error and fall back to using the full fragment
11994 try {
11995 range[setStart](el[rangeEl]);
11996 frag = range.createContextualFragment(html);
11997 }
11998 catch(e) {
11999 frag = this.createContextualFragment(html);
12000 }
12001 } else {
12002 frag = this.createContextualFragment(html);
12003 }
12004
12005 if (isAfterBegin) {
12006 el.insertBefore(frag, el.firstChild);
12007 } else {
12008 el.appendChild(frag);
12009 }
12010 } else {
12011 el.innerHTML = html;
12012 }
12013 return el[rangeEl];
12014 }
12015 },
12016
12017 /**
12018 * Creates new DOM element(s) and inserts them before el.
12019 * @param {String/HTMLElement/Ext.Element} el The context element
12020 * @param {Object/String} o The DOM object spec (and children) or raw HTML blob
12021 * @param {Boolean} [returnElement] true to return a Ext.Element
12022 * @return {HTMLElement/Ext.Element} The new node
12023 */
12024 insertBefore: function(el, o, returnElement) {
12025 return this.doInsert(el, o, returnElement, 'beforebegin');
12026 },
12027
12028 /**
12029 * Creates new DOM element(s) and inserts them after el.
12030 * @param {String/HTMLElement/Ext.Element} el The context element
12031 * @param {Object} o The DOM object spec (and children)
12032 * @param {Boolean} [returnElement] true to return a Ext.Element
12033 * @return {HTMLElement/Ext.Element} The new node
12034 */
12035 insertAfter: function(el, o, returnElement) {
12036 return this.doInsert(el, o, returnElement, 'afterend');
12037 },
12038
12039 /**
12040 * Creates new DOM element(s) and inserts them as the first child of el.
12041 * @param {String/HTMLElement/Ext.Element} el The context element
12042 * @param {Object/String} o The DOM object spec (and children) or raw HTML blob
12043 * @param {Boolean} [returnElement] true to return a Ext.Element
12044 * @return {HTMLElement/Ext.Element} The new node
12045 */
12046 insertFirst: function(el, o, returnElement) {
12047 return this.doInsert(el, o, returnElement, 'afterbegin');
12048 },
12049
12050 /**
12051 * Creates new DOM element(s) and appends them to el.
12052 * @param {String/HTMLElement/Ext.Element} el The context element
12053 * @param {Object/String} o The DOM object spec (and children) or raw HTML blob
12054 * @param {Boolean} [returnElement] true to return a Ext.Element
12055 * @return {HTMLElement/Ext.Element} The new node
12056 */
12057 append: function(el, o, returnElement) {
12058 return this.doInsert(el, o, returnElement, 'beforeend');
12059 },
12060
12061 /**
12062 * Creates new DOM element(s) and overwrites the contents of el with them.
12063 * @param {String/HTMLElement/Ext.Element} el The context element
12064 * @param {Object/String} o The DOM object spec (and children) or raw HTML blob
12065 * @param {Boolean} [returnElement] true to return a Ext.Element
12066 * @return {HTMLElement/Ext.Element} The new node
12067 */
12068 overwrite: function(el, o, returnElement) {
12069 el = Ext.getDom(el);
12070 el.innerHTML = this.markup(o);
12071 return returnElement ? Ext.get(el.firstChild) : el.firstChild;
12072 },
12073
12074 doInsert: function(el, o, returnElement, pos) {
12075 var newNode = this.insertHtml(pos, Ext.getDom(el), this.markup(o));
12076 return returnElement ? Ext.get(newNode, true) : newNode;
12077 },
12078
12079 /**
12080 * Creates a new Ext.Template from the DOM object spec.
12081 * @param {Object} o The DOM object spec (and children)
12082 * @return {Ext.Template} The new template
12083 */
12084 createTemplate: function(o) {
12085 var html = this.markup(o);
12086 return new Ext.Template(html);
12087 }
12088 }, function() {
12089 Ext.ns('Ext.core');
12090 Ext.core.DomHelper = Ext.DomHelper = new this;
12091 });
12092
12093 //@tag dom,core
12094 //@require Ext.dom.Helper
12095
12096 /**
12097 * An Identifiable mixin.
12098 * @private
12099 */
12100 Ext.define('Ext.mixin.Identifiable', {
12101 statics: {
12102 uniqueIds: {}
12103 },
12104
12105 isIdentifiable: true,
12106
12107 mixinId: 'identifiable',
12108
12109 idCleanRegex: /\.|[^\w\-]/g,
12110
12111 defaultIdPrefix: 'ext-',
12112
12113 defaultIdSeparator: '-',
12114
12115 getOptimizedId: function() {
12116 return this.id;
12117 },
12118
12119 getUniqueId: function() {
12120 var id = this.id,
12121 prototype, separator, xtype, uniqueIds, prefix;
12122
12123 if (!id) {
12124 prototype = this.self.prototype;
12125 separator = this.defaultIdSeparator;
12126
12127 uniqueIds = Ext.mixin.Identifiable.uniqueIds;
12128
12129 if (!prototype.hasOwnProperty('identifiablePrefix')) {
12130 xtype = this.xtype;
12131
12132 if (xtype) {
12133 prefix = this.defaultIdPrefix + xtype + separator;
12134 }
12135 else {
12136 prefix = prototype.$className.replace(this.idCleanRegex, separator).toLowerCase() + separator;
12137 }
12138
12139 prototype.identifiablePrefix = prefix;
12140 }
12141
12142 prefix = this.identifiablePrefix;
12143
12144 if (!uniqueIds.hasOwnProperty(prefix)) {
12145 uniqueIds[prefix] = 0;
12146 }
12147
12148 id = this.id = prefix + (++uniqueIds[prefix]);
12149 }
12150
12151 this.getUniqueId = this.getOptimizedId;
12152
12153 return id;
12154 },
12155
12156 setId: function(id) {
12157 this.id = id;
12158 },
12159
12160 /**
12161 * Retrieves the id of this component. Will autogenerate an id if one has not already been set.
12162 * @return {String} id
12163 */
12164 getId: function() {
12165 var id = this.id;
12166
12167 if (!id) {
12168 id = this.getUniqueId();
12169 }
12170
12171 this.getId = this.getOptimizedId;
12172
12173 return id;
12174 }
12175 });
12176
12177 //@tag dom,core
12178 //@define Ext.Element-all
12179 //@define Ext.Element
12180
12181 /**
12182 * Encapsulates a DOM element, adding simple DOM manipulation facilities, normalizing for browser differences.
12183 *
12184 * ## Usage
12185 *
12186 * // by id
12187 * var el = Ext.get("my-div");
12188 *
12189 * // by DOM element reference
12190 * var el = Ext.get(myDivElement);
12191 *
12192 * ## Composite (Collections of) Elements
12193 *
12194 * For working with collections of Elements, see {@link Ext.CompositeElement}.
12195 *
12196 * @mixins Ext.mixin.Observable
12197 */
12198 Ext.define('Ext.dom.Element', {
12199 alternateClassName: 'Ext.Element',
12200
12201 mixins: [
12202 Ext.mixin.Identifiable
12203 ],
12204
12205
12206
12207
12208
12209
12210 observableType: 'element',
12211
12212 xtype: 'element',
12213
12214 statics: {
12215 CREATE_ATTRIBUTES: {
12216 style: 'style',
12217 className: 'className',
12218 cls: 'cls',
12219 classList: 'classList',
12220 text: 'text',
12221 hidden: 'hidden',
12222 html: 'html',
12223 children: 'children'
12224 },
12225
12226 create: function(attributes, domNode) {
12227 var ATTRIBUTES = this.CREATE_ATTRIBUTES,
12228 element, elementStyle, tag, value, name, i, ln;
12229
12230 if (!attributes) {
12231 attributes = {};
12232 }
12233
12234 if (attributes.isElement) {
12235 return attributes.dom;
12236 }
12237 else if ('nodeType' in attributes) {
12238 return attributes;
12239 }
12240
12241 if (typeof attributes == 'string') {
12242 return document.createTextNode(attributes);
12243 }
12244
12245 tag = attributes.tag;
12246
12247 if (!tag) {
12248 tag = 'div';
12249 }
12250 if (attributes.namespace) {
12251 element = document.createElementNS(attributes.namespace, tag);
12252 } else {
12253 element = document.createElement(tag);
12254 }
12255 elementStyle = element.style;
12256
12257 for (name in attributes) {
12258 if (name != 'tag') {
12259 value = attributes[name];
12260
12261 switch (name) {
12262 case ATTRIBUTES.style:
12263 if (typeof value == 'string') {
12264 element.setAttribute(name, value);
12265 }
12266 else {
12267 for (i in value) {
12268 if (value.hasOwnProperty(i)) {
12269 elementStyle[i] = value[i];
12270 }
12271 }
12272 }
12273 break;
12274
12275 case ATTRIBUTES.className:
12276 case ATTRIBUTES.cls:
12277 element.className = value;
12278 break;
12279
12280 case ATTRIBUTES.classList:
12281 element.className = value.join(' ');
12282 break;
12283
12284 case ATTRIBUTES.text:
12285 element.textContent = value;
12286 break;
12287
12288 case ATTRIBUTES.hidden:
12289 if (value) {
12290 element.style.display = 'none';
12291 }
12292 break;
12293
12294 case ATTRIBUTES.html:
12295 element.innerHTML = value;
12296 break;
12297
12298 case ATTRIBUTES.children:
12299 for (i = 0,ln = value.length; i < ln; i++) {
12300 element.appendChild(this.create(value[i], true));
12301 }
12302 break;
12303
12304 default:
12305 element.setAttribute(name, value);
12306 }
12307 }
12308 }
12309
12310 if (domNode) {
12311 return element;
12312 }
12313 else {
12314 return this.get(element);
12315 }
12316 },
12317
12318 documentElement: null,
12319
12320 cache: {},
12321
12322 /**
12323 * Retrieves Ext.dom.Element objects. {@link Ext#get} is alias for {@link Ext.dom.Element#get}.
12324 *
12325 * Uses simple caching to consistently return the same object. Automatically fixes if an object was recreated with
12326 * the same id via AJAX or DOM.
12327 *
12328 * @param {String/HTMLElement/Ext.Element} element The `id` of the node, a DOM Node or an existing Element.
12329 * @return {Ext.dom.Element} The Element object (or `null` if no matching element was found).
12330 * @static
12331 * @inheritable
12332 */
12333 get: function(element) {
12334 var cache = this.cache,
12335 instance, dom, id;
12336
12337 if (!element) {
12338 return null;
12339 }
12340
12341 // DOM Id
12342 if (typeof element == 'string') {
12343 dom = document.getElementById(element);
12344
12345 if (cache.hasOwnProperty(element)) {
12346 instance = cache[element];
12347 }
12348
12349 // Is this in the DOM proper
12350 if (dom) {
12351 // Update our Ext Element dom reference with the true DOM (it may have changed)
12352 if (instance) {
12353 instance.dom = dom;
12354 }
12355 else {
12356 // Create a new instance of Ext Element
12357 instance = cache[element] = new this(dom);
12358 }
12359 }
12360 // Not in the DOM, but if its in the cache, we can still use that as a DOM fragment reference, otherwise null
12361 else if (!instance) {
12362 instance = null;
12363 }
12364
12365 return instance;
12366 }
12367
12368 // DOM element
12369 if ('tagName' in element) {
12370 id = element.id;
12371
12372 if (cache.hasOwnProperty(id)) {
12373 instance = cache[id];
12374 instance.dom = element;
12375 return instance;
12376 }
12377 else {
12378 instance = new this(element);
12379 cache[instance.getId()] = instance;
12380 }
12381
12382 return instance;
12383 }
12384
12385 // Ext Element
12386 if (element.isElement) {
12387 return element;
12388 }
12389
12390 // Ext Composite Element
12391 if (element.isComposite) {
12392 return element;
12393 }
12394
12395 // Array passed
12396 if (Ext.isArray(element)) {
12397 return this.select(element);
12398 }
12399
12400 // DOM Document
12401 if (element === document) {
12402 // create a bogus element object representing the document object
12403 if (!this.documentElement) {
12404 this.documentElement = new this(document.documentElement);
12405 this.documentElement.setId('ext-application');
12406 }
12407
12408 return this.documentElement;
12409 }
12410
12411 return null;
12412 },
12413
12414 data: function(element, key, value) {
12415 var cache = Ext.cache,
12416 id, data;
12417
12418 element = this.get(element);
12419
12420 if (!element) {
12421 return null;
12422 }
12423
12424 id = element.id;
12425
12426 data = cache[id].data;
12427
12428 if (!data) {
12429 cache[id].data = data = {};
12430 }
12431
12432 if (arguments.length == 2) {
12433 return data[key];
12434 }
12435 else {
12436 return (data[key] = value);
12437 }
12438 },
12439
12440 /**
12441 * Serializes a DOM form into a url encoded string
12442 * @param {Object} form The form
12443 * @return {String} The url encoded form
12444 */
12445 serializeForm : function(form) {
12446 var fElements = form.elements || (document.forms[form] || Ext.getDom(form)).elements,
12447 hasSubmit = false,
12448 encoder = encodeURIComponent,
12449 data = '',
12450 eLen = fElements.length,
12451 element, name, type, options, hasValue, e,
12452 o, oLen, opt;
12453
12454 for (e = 0; e < eLen; e++) {
12455 element = fElements[e];
12456 name = element.name;
12457 type = element.type;
12458 options = element.options;
12459
12460 if (!element.disabled && name) {
12461 if (/select-(one|multiple)/i.test(type)) {
12462 oLen = options.length;
12463 for (o = 0; o < oLen; o++) {
12464 opt = options[o];
12465 if (opt.selected) {
12466 hasValue = opt.hasAttribute ? opt.hasAttribute('value') : opt.getAttributeNode('value').specified;
12467 data += Ext.String.format('{0}={1}&', encoder(name), encoder(hasValue ? opt.value : opt.text));
12468 }
12469 }
12470 } else if (!(/file|undefined|reset|button/i.test(type))) {
12471 if (!(/radio|checkbox/i.test(type) && !element.checked) && !(type == 'submit' && hasSubmit)) {
12472 data += encoder(name) + '=' + encoder(element.value) + '&';
12473 hasSubmit = /submit/i.test(type);
12474 }
12475 }
12476 }
12477 }
12478 return data.substr(0, data.length - 1);
12479 },
12480
12481 /**
12482 * Serializes a DOM element and its children recursively into a string.
12483 * @param {Object} node DOM element to serialize.
12484 * @returns {String}
12485 */
12486 serializeNode: function (node) {
12487 var result = '',
12488 i, n, attr, child;
12489 if (node.nodeType === document.TEXT_NODE) {
12490 return node.nodeValue;
12491 }
12492 result += '<' + node.nodeName;
12493 if (node.attributes.length) {
12494 for (i = 0, n = node.attributes.length; i < n; i++) {
12495 attr = node.attributes[i];
12496 result += ' ' + attr.name + '="' + attr.value + '"';
12497 }
12498 }
12499 result += '>';
12500 if (node.childNodes && node.childNodes.length) {
12501 for (i = 0, n = node.childNodes.length; i < n; i++) {
12502 child = node.childNodes[i];
12503 result += this.serializeNode(child);
12504 }
12505 }
12506 result += '</' + node.nodeName + '>';
12507 return result;
12508 }
12509 },
12510
12511 isElement: true,
12512
12513
12514 /**
12515 * @event painted
12516 * Fires whenever this Element actually becomes visible (painted) on the screen. This is useful when you need to
12517 * perform 'read' operations on the DOM element, i.e: calculating natural sizes and positioning.
12518 *
12519 * __Note:__ This event is not available to be used with event delegation. Instead `painted` only fires if you explicitly
12520 * add at least one listener to it, for performance reasons.
12521 *
12522 * @param {Ext.Element} this The component instance.
12523 */
12524
12525 /**
12526 * @event resize
12527 * Important note: For the best performance on mobile devices, use this only when you absolutely need to monitor
12528 * a Element's size.
12529 *
12530 * __Note:__ This event is not available to be used with event delegation. Instead `resize` only fires if you explicitly
12531 * add at least one listener to it, for performance reasons.
12532 *
12533 * @param {Ext.Element} this The component instance.
12534 */
12535
12536 constructor: function(dom) {
12537 if (typeof dom == 'string') {
12538 dom = document.getElementById(dom);
12539 }
12540
12541 if (!dom) {
12542 throw new Error("Invalid domNode reference or an id of an existing domNode: " + dom);
12543 }
12544
12545 /**
12546 * The DOM element
12547 * @property dom
12548 * @type HTMLElement
12549 */
12550 this.dom = dom;
12551
12552 this.getUniqueId();
12553 },
12554
12555 attach: function (dom) {
12556 this.dom = dom;
12557 this.id = dom.id;
12558 return this;
12559 },
12560
12561 getUniqueId: function() {
12562 var id = this.id,
12563 dom;
12564
12565 if (!id) {
12566 dom = this.dom;
12567
12568 if (dom.id.length > 0) {
12569 this.id = id = dom.id;
12570 }
12571 else {
12572 dom.id = id = this.mixins.identifiable.getUniqueId.call(this);
12573 }
12574
12575 Ext.Element.cache[id] = this;
12576 }
12577
12578 return id;
12579 },
12580
12581 setId: function(id) {
12582 var currentId = this.id,
12583 cache = Ext.Element.cache;
12584
12585 if (currentId) {
12586 delete cache[currentId];
12587 }
12588
12589 this.dom.id = id;
12590
12591 /**
12592 * The DOM element ID
12593 * @property id
12594 * @type String
12595 */
12596 this.id = id;
12597
12598 cache[id] = this;
12599
12600 return this;
12601 },
12602
12603 /**
12604 * Sets the `innerHTML` of this element.
12605 * @param {String} html The new HTML.
12606 */
12607 setHtml: function(html) {
12608 this.dom.innerHTML = html;
12609 },
12610
12611 /**
12612 * Returns the `innerHTML` of an element.
12613 * @return {String}
12614 */
12615 getHtml: function() {
12616 return this.dom.innerHTML;
12617 },
12618
12619 setText: function(text) {
12620 this.dom.textContent = text;
12621 },
12622
12623 redraw: function() {
12624 var dom = this.dom,
12625 domStyle = dom.style;
12626
12627 domStyle.display = 'none';
12628 dom.offsetHeight;
12629 domStyle.display = '';
12630 },
12631
12632 isPainted: (function() {
12633 return !Ext.browser.is.IE ? function() {
12634 var dom = this.dom;
12635 return Boolean(dom && dom.offsetParent);
12636 } : function() {
12637 var dom = this.dom;
12638 return Boolean(dom && (dom.offsetHeight !== 0 && dom.offsetWidth !== 0));
12639 }
12640 })(),
12641
12642 /**
12643 * Sets the passed attributes as attributes of this element (a style attribute can be a string, object or function).
12644 * @param {Object} attributes The object with the attributes.
12645 * @param {Boolean} [useSet=true] `false` to override the default `setAttribute` to use expandos.
12646 * @return {Ext.dom.Element} this
12647 */
12648 set: function(attributes, useSet) {
12649 var dom = this.dom,
12650 attribute, value;
12651
12652 for (attribute in attributes) {
12653 if (attributes.hasOwnProperty(attribute)) {
12654 value = attributes[attribute];
12655
12656 if (attribute == 'style') {
12657 this.applyStyles(value);
12658 }
12659 else if (attribute == 'cls') {
12660 dom.className = value;
12661 }
12662 else if (useSet !== false) {
12663 if (value === undefined) {
12664 dom.removeAttribute(attribute);
12665 } else {
12666 dom.setAttribute(attribute, value);
12667 }
12668 }
12669 else {
12670 dom[attribute] = value;
12671 }
12672 }
12673 }
12674
12675 return this;
12676 },
12677
12678 /**
12679 * Returns `true` if this element matches the passed simple selector (e.g. 'div.some-class' or 'span:first-child').
12680 * @param {String} selector The simple selector to test.
12681 * @return {Boolean} `true` if this element matches the selector, else `false`.
12682 */
12683 is: function(selector) {
12684 return Ext.DomQuery.is(this.dom, selector);
12685 },
12686
12687 /**
12688 * Returns the value of the `value` attribute.
12689 * @param {Boolean} asNumber `true` to parse the value as a number.
12690 * @return {String/Number}
12691 */
12692 getValue: function(asNumber) {
12693 var value = this.dom.value;
12694
12695 return asNumber ? parseInt(value, 10) : value;
12696 },
12697
12698 /**
12699 * Returns the value of an attribute from the element's underlying DOM node.
12700 * @param {String} name The attribute name.
12701 * @param {String} [namespace] The namespace in which to look for the attribute.
12702 * @return {String} The attribute value.
12703 */
12704 getAttribute: function(name, namespace) {
12705 var dom = this.dom;
12706
12707 return dom.getAttributeNS(namespace, name) || dom.getAttribute(namespace + ":" + name)
12708 || dom.getAttribute(name) || dom[name];
12709 },
12710
12711 setSizeState: function(state) {
12712 var classes = ['x-sized', 'x-unsized', 'x-stretched'],
12713 states = [true, false, null],
12714 index = states.indexOf(state),
12715 addedClass;
12716
12717 if (index !== -1) {
12718 addedClass = classes[index];
12719 classes.splice(index, 1);
12720 this.addCls(addedClass);
12721 }
12722
12723 this.removeCls(classes);
12724
12725 return this;
12726 },
12727
12728 /**
12729 * Removes this element's DOM reference. Note that event and cache removal is handled at {@link Ext#removeNode}
12730 */
12731 destroy: function() {
12732 this.isDestroyed = true;
12733
12734 var cache = Ext.Element.cache,
12735 dom = this.dom;
12736
12737 if (dom && dom.parentNode && dom.tagName != 'BODY') {
12738 dom.parentNode.removeChild(dom);
12739 }
12740
12741 delete cache[this.id];
12742 delete this.dom;
12743 }
12744
12745 }, function(Element) {
12746 Ext.elements = Ext.cache = Element.cache;
12747
12748 this.addStatics({
12749 Fly: new Ext.Class({
12750 extend: Element,
12751
12752 constructor: function(dom) {
12753 this.dom = dom;
12754 }
12755 }),
12756
12757 _flyweights: {},
12758
12759 /**
12760 * Gets the globally shared flyweight Element, with the passed node as the active element. Do not store a reference
12761 * to this element - the dom node can be overwritten by other code. {@link Ext#fly} is alias for
12762 * {@link Ext.dom.Element#fly}.
12763 *
12764 * Use this to make one-time references to DOM elements which are not going to be accessed again either by
12765 * application code, or by Ext's classes. If accessing an element which will be processed regularly, then {@link Ext#get Ext.get}
12766 * will be more appropriate to take advantage of the caching provided by the {@link Ext.dom.Element}
12767 * class.
12768 *
12769 * @param {String/HTMLElement} element The DOM node or `id`.
12770 * @param {String} [named] Allows for creation of named reusable flyweights to prevent conflicts (e.g.
12771 * internally Ext uses "_global").
12772 * @return {Ext.dom.Element} The shared Element object (or `null` if no matching element was found).
12773 * @static
12774 */
12775 fly: function(element, named) {
12776 var fly = null,
12777 flyweights = Element._flyweights,
12778 cachedElement;
12779
12780 named = named || '_global';
12781
12782 element = Ext.getDom(element);
12783
12784 if (element) {
12785 fly = flyweights[named] || (flyweights[named] = new Element.Fly());
12786 fly.dom = element;
12787 fly.isSynchronized = false;
12788 cachedElement = Ext.cache[element.id];
12789 if (cachedElement && cachedElement.isElement) {
12790 cachedElement.isSynchronized = false;
12791 }
12792 }
12793
12794 return fly;
12795 }
12796 });
12797
12798 /**
12799 * @member Ext
12800 * @method get
12801 * @alias Ext.dom.Element#get
12802 */
12803 Ext.get = function(element) {
12804 return Element.get(element);
12805 };
12806
12807 /**
12808 * @member Ext
12809 * @method fly
12810 * @alias Ext.dom.Element#fly
12811 */
12812 Ext.fly = function() {
12813 return Element.fly.apply(Element, arguments);
12814 };
12815
12816 Ext.ClassManager.onCreated(function() {
12817 Element.mixin('observable', Ext.mixin.Observable);
12818 }, null, 'Ext.mixin.Observable');
12819
12820
12821 });
12822
12823 //@tag dom,core
12824 //@define Ext.Element-all
12825 //@define Ext.Element-static
12826 //@require Ext.Element
12827
12828 /**
12829 * @class Ext.dom.Element
12830 */
12831 Ext.dom.Element.addStatics({
12832 numberRe: /\d+$/,
12833 unitRe: /\d+(px|em|%|en|ex|pt|in|cm|mm|pc)$/i,
12834 camelRe: /(-[a-z])/gi,
12835 cssRe: /([a-z0-9-]+)\s*:\s*([^;\s]+(?:\s*[^;\s]+)*);?/gi,
12836 opacityRe: /alpha\(opacity=(.*)\)/i,
12837 propertyCache: {},
12838 defaultUnit: "px",
12839 borders: {l: 'border-left-width', r: 'border-right-width', t: 'border-top-width', b: 'border-bottom-width'},
12840 paddings: {l: 'padding-left', r: 'padding-right', t: 'padding-top', b: 'padding-bottom'},
12841 margins: {l: 'margin-left', r: 'margin-right', t: 'margin-top', b: 'margin-bottom'},
12842
12843 /**
12844 * Test if size has a unit, otherwise appends the passed unit string, or the default for this Element.
12845 * @param {Object} size The size to set.
12846 * @param {String} units The units to append to a numeric size value.
12847 * @return {String}
12848 * @private
12849 * @static
12850 */
12851 addUnits: function(size, units) {
12852 // Size set to a value which means "auto"
12853 if (size === "" || size == "auto" || size === undefined || size === null) {
12854 return size || '';
12855 }
12856
12857 // Otherwise, warn if it's not a valid CSS measurement
12858 if (Ext.isNumber(size) || this.numberRe.test(size)) {
12859 return size + (units || this.defaultUnit || 'px');
12860 }
12861 else if (!this.unitRe.test(size)) {
12862 //<debug>
12863 Ext.Logger.warn("Warning, size detected (" + size + ") not a valid property value on Element.addUnits.");
12864 //</debug>
12865 return size || '';
12866 }
12867
12868 return size;
12869 },
12870
12871 /**
12872 * @static
12873 * @return {Boolean}
12874 * @private
12875 */
12876 isAncestor: function(p, c) {
12877 var ret = false;
12878
12879 p = Ext.getDom(p);
12880 c = Ext.getDom(c);
12881 if (p && c) {
12882 if (p.contains) {
12883 return p.contains(c);
12884 } else if (p.compareDocumentPosition) {
12885 return !!(p.compareDocumentPosition(c) & 16);
12886 } else {
12887 while ((c = c.parentNode)) {
12888 ret = c == p || ret;
12889 }
12890 }
12891 }
12892 return ret;
12893 },
12894
12895 /**
12896 * Parses a number or string representing margin sizes into an object. Supports CSS-style margin declarations
12897 * (e.g. 10, "10", "10 10", "10 10 10" and "10 10 10 10" are all valid options and would return the same result)
12898 * @static
12899 * @param {Number/String} box The encoded margins
12900 * @return {Object} An object with margin sizes for top, right, bottom and left containing the unit
12901 */
12902 parseBox: function(box) {
12903 if (typeof box != 'string') {
12904 box = box.toString();
12905 }
12906
12907 var parts = box.split(' '),
12908 ln = parts.length;
12909
12910 if (ln == 1) {
12911 parts[1] = parts[2] = parts[3] = parts[0];
12912 }
12913 else if (ln == 2) {
12914 parts[2] = parts[0];
12915 parts[3] = parts[1];
12916 }
12917 else if (ln == 3) {
12918 parts[3] = parts[1];
12919 }
12920
12921 return {
12922 top: parts[0] || 0,
12923 right: parts[1] || 0,
12924 bottom: parts[2] || 0,
12925 left: parts[3] || 0
12926 };
12927 },
12928
12929 /**
12930 * Parses a number or string representing margin sizes into an object. Supports CSS-style margin declarations
12931 * (e.g. 10, "10", "10 10", "10 10 10" and "10 10 10 10" are all valid options and would return the same result)
12932 * @static
12933 * @param {Number/String} box The encoded margins
12934 * @param {String} units The type of units to add
12935 * @return {String} An string with unitized (px if units is not specified) metrics for top, right, bottom and left
12936 */
12937 unitizeBox: function(box, units) {
12938 var me = this;
12939 box = me.parseBox(box);
12940
12941 return me.addUnits(box.top, units) + ' ' +
12942 me.addUnits(box.right, units) + ' ' +
12943 me.addUnits(box.bottom, units) + ' ' +
12944 me.addUnits(box.left, units);
12945 },
12946
12947 // @private
12948 camelReplaceFn: function(m, a) {
12949 return a.charAt(1).toUpperCase();
12950 },
12951
12952 /**
12953 * Normalizes CSS property keys from dash delimited to camel case JavaScript Syntax.
12954 * For example:
12955 *
12956 * - border-width -> borderWidth
12957 * - padding-top -> paddingTop
12958 *
12959 * @static
12960 * @param {String} prop The property to normalize
12961 * @return {String} The normalized string
12962 */
12963 normalize: function(prop) {
12964 // TODO: Mobile optimization?
12965 // if (prop == 'float') {
12966 // prop = Ext.supports.Float ? 'cssFloat' : 'styleFloat';
12967 // }
12968 return this.propertyCache[prop] || (this.propertyCache[prop] = prop.replace(this.camelRe, this.camelReplaceFn));
12969 },
12970
12971 /**
12972 * Returns the top Element that is located at the passed coordinates
12973 * @static
12974 * @param {Number} x The x coordinate
12975 * @param {Number} y The y coordinate
12976 * @return {String} The found Element
12977 */
12978 fromPoint: function(x, y) {
12979 return Ext.get(document.elementFromPoint(x, y));
12980 },
12981
12982 /**
12983 * Converts a CSS string into an object with a property for each style.
12984 *
12985 * The sample code below would return an object with 2 properties, one
12986 * for background-color and one for color.
12987 *
12988 * var css = 'background-color: red;color: blue; ';
12989 * console.log(Ext.dom.Element.parseStyles(css));
12990 *
12991 * @static
12992 * @param {String} styles A CSS string
12993 * @return {Object} styles
12994 */
12995 parseStyles: function(styles) {
12996 var out = {},
12997 cssRe = this.cssRe,
12998 matches;
12999
13000 if (styles) {
13001 // Since we're using the g flag on the regex, we need to set the lastIndex.
13002 // This automatically happens on some implementations, but not others, see:
13003 // http://stackoverflow.com/questions/2645273/javascript-regular-expression-literal-persists-between-function-calls
13004 // http://blog.stevenlevithan.com/archives/fixing-javascript-regexp
13005 cssRe.lastIndex = 0;
13006 while ((matches = cssRe.exec(styles))) {
13007 out[matches[1]] = matches[2];
13008 }
13009 }
13010 return out;
13011 }
13012 });
13013
13014
13015 //@tag dom,core
13016 //@define Ext.Element-all
13017 //@define Ext.Element-alignment
13018 //@require Ext.Element-static
13019
13020 /**
13021 * @class Ext.dom.Element
13022 */
13023
13024 //@tag dom,core
13025 //@define Ext.Element-all
13026 //@define Ext.Element-insertion
13027 //@require Ext.Element-alignment
13028
13029 /**
13030 * @class Ext.dom.Element
13031 */
13032 Ext.dom.Element.addMembers({
13033
13034 /**
13035 * Appends the passed element(s) to this element.
13036 * @param {HTMLElement/Ext.dom.Element} element a DOM Node or an existing Element.
13037 * @return {Ext.dom.Element} This element.
13038 */
13039 appendChild: function(element) {
13040 this.dom.appendChild(Ext.getDom(element));
13041
13042 return this;
13043 },
13044
13045 removeChild: function(element) {
13046 this.dom.removeChild(Ext.getDom(element));
13047
13048 return this;
13049 },
13050
13051 append: function() {
13052 this.appendChild.apply(this, arguments);
13053 },
13054
13055 /**
13056 * Appends this element to the passed element.
13057 * @param {String/HTMLElement/Ext.dom.Element} el The new parent element.
13058 * The id of the node, a DOM Node or an existing Element.
13059 * @return {Ext.dom.Element} This element.
13060 */
13061 appendTo: function(el) {
13062 Ext.getDom(el).appendChild(this.dom);
13063 return this;
13064 },
13065
13066 /**
13067 * Inserts this element before the passed element in the DOM.
13068 * @param {String/HTMLElement/Ext.dom.Element} el The element before which this element will be inserted.
13069 * The id of the node, a DOM Node or an existing Element.
13070 * @return {Ext.dom.Element} This element.
13071 */
13072 insertBefore: function(el) {
13073 el = Ext.getDom(el);
13074 el.parentNode.insertBefore(this.dom, el);
13075 return this;
13076 },
13077
13078 /**
13079 * Inserts this element after the passed element in the DOM.
13080 * @param {String/HTMLElement/Ext.dom.Element} el The element to insert after.
13081 * The `id` of the node, a DOM Node or an existing Element.
13082 * @return {Ext.dom.Element} This element.
13083 */
13084 insertAfter: function(el) {
13085 el = Ext.getDom(el);
13086 el.parentNode.insertBefore(this.dom, el.nextSibling);
13087 return this;
13088 },
13089
13090
13091 /**
13092 * Inserts an element as the first child of this element.
13093 * @param {String/HTMLElement/Ext.dom.Element} element The `id` or element to insert.
13094 * @return {Ext.dom.Element} this
13095 */
13096 insertFirst: function(element) {
13097 var elementDom = Ext.getDom(element),
13098 dom = this.dom,
13099 firstChild = dom.firstChild;
13100
13101 if (!firstChild) {
13102 dom.appendChild(elementDom);
13103 }
13104 else {
13105 dom.insertBefore(elementDom, firstChild);
13106 }
13107
13108 return this;
13109 },
13110
13111 /**
13112 * Inserts (or creates) the passed element (or DomHelper config) as a sibling of this element
13113 * @param {String/HTMLElement/Ext.dom.Element/Object/Array} el The id, element to insert or a DomHelper config
13114 * to create and insert *or* an array of any of those.
13115 * @param {String} [where=before] (optional) 'before' or 'after'.
13116 * @param {Boolean} returnDom (optional) `true` to return the raw DOM element instead of Ext.dom.Element.
13117 * @return {Ext.dom.Element} The inserted Element. If an array is passed, the last inserted element is returned.
13118 */
13119 insertSibling: function(el, where, returnDom) {
13120 var me = this, rt,
13121 isAfter = (where || 'before').toLowerCase() == 'after',
13122 insertEl;
13123
13124 if (Ext.isArray(el)) {
13125 insertEl = me;
13126 Ext.each(el, function(e) {
13127 rt = Ext.fly(insertEl, '_internal').insertSibling(e, where, returnDom);
13128 if (isAfter) {
13129 insertEl = rt;
13130 }
13131 });
13132 return rt;
13133 }
13134
13135 el = el || {};
13136
13137 if (el.nodeType || el.dom) {
13138 rt = me.dom.parentNode.insertBefore(Ext.getDom(el), isAfter ? me.dom.nextSibling : me.dom);
13139 if (!returnDom) {
13140 rt = Ext.get(rt);
13141 }
13142 } else {
13143 if (isAfter && !me.dom.nextSibling) {
13144 rt = Ext.core.DomHelper.append(me.dom.parentNode, el, !returnDom);
13145 } else {
13146 rt = Ext.core.DomHelper[isAfter ? 'insertAfter' : 'insertBefore'](me.dom, el, !returnDom);
13147 }
13148 }
13149 return rt;
13150 },
13151
13152 /**
13153 * Replaces the passed element with this element.
13154 * @param {String/HTMLElement/Ext.dom.Element} element The element to replace.
13155 * The id of the node, a DOM Node or an existing Element.
13156 * @return {Ext.dom.Element} This element.
13157 */
13158 replace: function(element) {
13159 element = Ext.getDom(element);
13160
13161 element.parentNode.replaceChild(this.dom, element);
13162
13163 return this;
13164 },
13165
13166 /**
13167 * Replaces this element with the passed element.
13168 * @param {String/HTMLElement/Ext.dom.Element/Object} el The new element (id of the node, a DOM Node
13169 * or an existing Element) or a DomHelper config of an element to create.
13170 * @return {Ext.dom.Element} This element.
13171 */
13172 replaceWith: function(el) {
13173 var me = this;
13174
13175 if (el.nodeType || el.dom || typeof el == 'string') {
13176 el = Ext.get(el);
13177 me.dom.parentNode.insertBefore(el.dom, me.dom);
13178 } else {
13179 el = Ext.core.DomHelper.insertBefore(me.dom, el);
13180 }
13181
13182 delete Ext.cache[me.id];
13183 Ext.removeNode(me.dom);
13184 me.id = Ext.id(me.dom = el);
13185 return me;
13186 },
13187
13188 doReplaceWith: function(element) {
13189 var dom = this.dom;
13190 dom.parentNode.replaceChild(Ext.getDom(element), dom);
13191 },
13192
13193 /**
13194 * Creates the passed DomHelper config and appends it to this element or optionally inserts it before the passed child element.
13195 * @param {Object} config DomHelper element config object. If no tag is specified (e.g., `{tag:'input'}`) then a div will be
13196 * automatically generated with the specified attributes.
13197 * @param {HTMLElement} insertBefore (optional) a child element of this element.
13198 * @param {Boolean} returnDom (optional) `true` to return the dom node instead of creating an Element.
13199 * @return {Ext.dom.Element} The new child element.
13200 */
13201 createChild: function(config, insertBefore, returnDom) {
13202 config = config || {tag: 'div'};
13203 if (insertBefore) {
13204 return Ext.core.DomHelper.insertBefore(insertBefore, config, returnDom !== true);
13205 }
13206 else {
13207 return Ext.core.DomHelper[!this.dom.firstChild ? 'insertFirst' : 'append'](this.dom, config, returnDom !== true);
13208 }
13209 },
13210
13211 /**
13212 * Creates and wraps this element with another element.
13213 * @param {Object} [config] (optional) DomHelper element config object for the wrapper element or `null` for an empty div
13214 * @param {Boolean} [domNode] (optional) `true` to return the raw DOM element instead of Ext.dom.Element.
13215 * @return {HTMLElement/Ext.dom.Element} The newly created wrapper element.
13216 */
13217 wrap: function(config, domNode) {
13218 var dom = this.dom,
13219 wrapper = this.self.create(config, domNode),
13220 wrapperDom = (domNode) ? wrapper : wrapper.dom,
13221 parentNode = dom.parentNode;
13222
13223 if (parentNode) {
13224 parentNode.insertBefore(wrapperDom, dom);
13225 }
13226
13227 wrapperDom.appendChild(dom);
13228
13229 return wrapper;
13230 },
13231
13232 wrapAllChildren: function(config) {
13233 var dom = this.dom,
13234 children = dom.childNodes,
13235 wrapper = this.self.create(config),
13236 wrapperDom = wrapper.dom;
13237
13238 while (children.length > 0) {
13239 wrapperDom.appendChild(dom.firstChild);
13240 }
13241
13242 dom.appendChild(wrapperDom);
13243
13244 return wrapper;
13245 },
13246
13247 unwrapAllChildren: function() {
13248 var dom = this.dom,
13249 children = dom.childNodes,
13250 parentNode = dom.parentNode;
13251
13252 if (parentNode) {
13253 while (children.length > 0) {
13254 parentNode.insertBefore(dom, dom.firstChild);
13255 }
13256
13257 this.destroy();
13258 }
13259 },
13260
13261 unwrap: function() {
13262 var dom = this.dom,
13263 parentNode = dom.parentNode,
13264 grandparentNode;
13265
13266 if (parentNode) {
13267 grandparentNode = parentNode.parentNode;
13268 grandparentNode.insertBefore(dom, parentNode);
13269 grandparentNode.removeChild(parentNode);
13270 }
13271 else {
13272 grandparentNode = document.createDocumentFragment();
13273 grandparentNode.appendChild(dom);
13274 }
13275
13276 return this;
13277 },
13278
13279 detach: function() {
13280 var dom = this.dom;
13281
13282 if (dom && dom.parentNode && dom.tagName !== 'BODY') {
13283 dom.parentNode.removeChild(dom);
13284 }
13285
13286 return this;
13287 },
13288
13289 /**
13290 * Inserts an HTML fragment into this element.
13291 * @param {String} where Where to insert the HTML in relation to this element - 'beforeBegin', 'afterBegin', 'beforeEnd', 'afterEnd'.
13292 * See {@link Ext.DomHelper#insertHtml} for details.
13293 * @param {String} html The HTML fragment
13294 * @param {Boolean} [returnEl=false] (optional) `true` to return an Ext.dom.Element.
13295 * @return {HTMLElement/Ext.dom.Element} The inserted node (or nearest related if more than 1 inserted).
13296 */
13297 insertHtml: function(where, html, returnEl) {
13298 var el = Ext.core.DomHelper.insertHtml(where, this.dom, html);
13299 return returnEl ? Ext.get(el) : el;
13300 }
13301 });
13302
13303 //@tag dom,core
13304 //@define Ext.Element-all
13305 //@define Ext.Element-position
13306 //@require Ext.Element-insertion
13307
13308 /**
13309 * @class Ext.dom.Element
13310 */
13311 Ext.dom.Element.override({
13312
13313 /**
13314 * Gets the current X position of the element based on page coordinates. Element must be part of the DOM tree to have page coordinates (`display:none` or elements not appended return `false`).
13315 * @return {Number} The X position of the element
13316 */
13317 getX: function() {
13318 return this.getXY()[0];
13319 },
13320
13321 /**
13322 * Gets the current Y position of the element based on page coordinates. Element must be part of the DOM tree to have page coordinates (`display:none` or elements not appended return `false`).
13323 * @return {Number} The Y position of the element
13324 */
13325 getY: function() {
13326 return this.getXY()[1];
13327 },
13328
13329 /**
13330 * Gets the current position of the element based on page coordinates. Element must be part of the DOM tree to have page coordinates (`display:none` or elements not appended return `false`).
13331 * @return {Array} The XY position of the element
13332 */
13333
13334 getXY: function() {
13335 var rect = this.dom.getBoundingClientRect(),
13336 round = Math.round;
13337
13338 return [round(rect.left + window.pageXOffset), round(rect.top + window.pageYOffset)];
13339 },
13340
13341 /**
13342 * Returns the offsets of this element from the passed element. Both element must be part of the DOM tree
13343 * and not have `display:none` to have page coordinates.
13344 * @param {Mixed} element The element to get the offsets from.
13345 * @return {Array} The XY page offsets (e.g. [100, -200])
13346 */
13347 getOffsetsTo: function(el) {
13348 var o = this.getXY(),
13349 e = Ext.fly(el, '_internal').getXY();
13350 return [o[0] - e[0], o[1] - e[1]];
13351 },
13352
13353 /**
13354 * Sets the X position of the element based on page coordinates. Element must be part of the DOM tree to have page coordinates (`display:none` or elements not appended return `false`).
13355 * @param {Number} x The X position of the element
13356 * @return {Ext.dom.Element} this
13357 */
13358 setX: function(x) {
13359 return this.setXY([x, this.getY()]);
13360 },
13361
13362 /**
13363 * Sets the Y position of the element based on page coordinates. Element must be part of the DOM tree to have page coordinates (`display:none` or elements not appended return `false`).
13364 * @param {Number} y The Y position of the element.
13365 * @return {Ext.dom.Element} this
13366 */
13367 setY: function(y) {
13368 return this.setXY([this.getX(), y]);
13369 },
13370
13371 /**
13372 * Sets the position of the element in page coordinates, regardless of how the element is positioned.
13373 * The element must be part of the DOM tree to have page coordinates (`display:none` or elements not appended return `false`).
13374 * @param {Number[]} pos Contains X & Y [x, y] values for new position (coordinates are page-based).
13375 * @return {Ext.dom.Element} this
13376 */
13377 setXY: function(pos) {
13378 var me = this;
13379
13380 if (arguments.length > 1) {
13381 pos = [pos, arguments[1]];
13382 }
13383
13384 // me.position();
13385 var pts = me.translatePoints(pos),
13386 style = me.dom.style;
13387
13388 for (pos in pts) {
13389 if (!pts.hasOwnProperty(pos)) {
13390 continue;
13391 }
13392 if (!isNaN(pts[pos])) style[pos] = pts[pos] + "px";
13393 }
13394 return me;
13395 },
13396
13397 /**
13398 * Gets the left X coordinate.
13399 * @return {Number}
13400 */
13401 getLeft: function() {
13402 return parseInt(this.getStyle('left'), 10) || 0;
13403 },
13404
13405 /**
13406 * Gets the right X coordinate of the element (element X position + element width).
13407 * @return {Number}
13408 */
13409 getRight: function() {
13410 return parseInt(this.getStyle('right'), 10) || 0;
13411 },
13412
13413 /**
13414 * Gets the top Y coordinate.
13415 * @return {Number}
13416 */
13417 getTop: function() {
13418 return parseInt(this.getStyle('top'), 10) || 0;
13419 },
13420
13421 /**
13422 * Gets the bottom Y coordinate of the element (element Y position + element height).
13423 * @return {Number}
13424 */
13425 getBottom: function() {
13426 return parseInt(this.getStyle('bottom'), 10) || 0;
13427 },
13428
13429 /**
13430 * Translates the passed page coordinates into left/top CSS values for this element.
13431 * @param {Number/Array} x The page `x` or an array containing [x, y].
13432 * @param {Number} y (optional) The page `y`, required if `x` is not an array.
13433 * @return {Object} An object with `left` and `top` properties. e.g. `{left: (value), top: (value)}`.
13434 */
13435 translatePoints: function(x, y) {
13436 y = isNaN(x[1]) ? y : x[1];
13437 x = isNaN(x[0]) ? x : x[0];
13438
13439 var me = this,
13440 relative = me.isStyle('position', 'relative'),
13441 o = me.getXY(),
13442 l = parseInt(me.getStyle('left'), 10),
13443 t = parseInt(me.getStyle('top'), 10);
13444
13445 l = !isNaN(l) ? l : (relative ? 0 : me.dom.offsetLeft);
13446 t = !isNaN(t) ? t : (relative ? 0 : me.dom.offsetTop);
13447
13448 return {left: (x - o[0] + l), top: (y - o[1] + t)};
13449 },
13450
13451 /**
13452 * Sets the element's box. Use {@link #getBox} on another element to get a box object.
13453 * @param {Object} box The box to fill, for example:
13454 *
13455 * {
13456 * left: ...,
13457 * top: ...,
13458 * width: ...,
13459 * height: ...
13460 * }
13461 *
13462 * @return {Ext.dom.Element} this
13463 */
13464 setBox: function(box) {
13465 var me = this,
13466 width = box.width,
13467 height = box.height,
13468 top = box.top,
13469 left = box.left;
13470
13471 if (left !== undefined) {
13472 me.setLeft(left);
13473 }
13474 if (top !== undefined) {
13475 me.setTop(top);
13476 }
13477 if (width !== undefined) {
13478 me.setWidth(width);
13479 }
13480 if (height !== undefined) {
13481 me.setHeight(height);
13482 }
13483
13484 return this;
13485 },
13486
13487 /**
13488 * Return an object defining the area of this Element which can be passed to {@link #setBox} to
13489 * set another Element's size/location to match this element.
13490 *
13491 * The returned object may also be addressed as an Array where index 0 contains the X position
13492 * and index 1 contains the Y position. So the result may also be used for {@link #setXY}.
13493 *
13494 * @param {Boolean} contentBox (optional) If `true` a box for the content of the element is returned.
13495 * @param {Boolean} local (optional) If `true` the element's left and top are returned instead of page x/y.
13496 * @return {Object} An object in the format
13497 * @return {Number} return.x The element's X position.
13498 * @return {Number} return.y The element's Y position.
13499 * @return {Number} return.width The element's width.
13500 * @return {Number} return.height The element's height.
13501 * @return {Number} return.bottom The element's lower bound.
13502 * @return {Number} return.right The element's rightmost bound.
13503 */
13504 getBox: function(contentBox, local) {
13505 var me = this,
13506 dom = me.dom,
13507 width = dom.offsetWidth,
13508 height = dom.offsetHeight,
13509 xy, box, l, r, t, b;
13510
13511 if (!local) {
13512 xy = me.getXY();
13513 }
13514 else if (contentBox) {
13515 xy = [0, 0];
13516 }
13517 else {
13518 xy = [parseInt(me.getStyle("left"), 10) || 0, parseInt(me.getStyle("top"), 10) || 0];
13519 }
13520
13521 if (!contentBox) {
13522 box = {
13523 x: xy[0],
13524 y: xy[1],
13525 0: xy[0],
13526 1: xy[1],
13527 width: width,
13528 height: height
13529 };
13530 }
13531 else {
13532 l = me.getBorderWidth.call(me, "l") + me.getPadding.call(me, "l");
13533 r = me.getBorderWidth.call(me, "r") + me.getPadding.call(me, "r");
13534 t = me.getBorderWidth.call(me, "t") + me.getPadding.call(me, "t");
13535 b = me.getBorderWidth.call(me, "b") + me.getPadding.call(me, "b");
13536 box = {
13537 x: xy[0] + l,
13538 y: xy[1] + t,
13539 0: xy[0] + l,
13540 1: xy[1] + t,
13541 width: width - (l + r),
13542 height: height - (t + b)
13543 };
13544 }
13545
13546 box.left = box.x;
13547 box.top = box.y;
13548 box.right = box.x + box.width;
13549 box.bottom = box.y + box.height;
13550
13551 return box;
13552 },
13553
13554 /**
13555 * Return an object defining the area of this Element which can be passed to {@link #setBox} to
13556 * set another Element's size/location to match this element.
13557 * @param {Boolean} asRegion (optional) If `true` an {@link Ext.util.Region} will be returned.
13558 * @return {Object} box An object in the format:
13559 *
13560 * {
13561 * x: <Element's X position>,
13562 * y: <Element's Y position>,
13563 * width: <Element's width>,
13564 * height: <Element's height>,
13565 * bottom: <Element's lower bound>,
13566 * right: <Element's rightmost bound>
13567 * }
13568 *
13569 * The returned object may also be addressed as an Array where index 0 contains the X position
13570 * and index 1 contains the Y position. So the result may also be used for {@link #setXY}.
13571 */
13572 getPageBox: function(getRegion) {
13573 var me = this,
13574 el = me.dom;
13575
13576 if (!el) {
13577 return new Ext.util.Region();
13578 }
13579
13580 var w = el.offsetWidth,
13581 h = el.offsetHeight,
13582 xy = me.getXY(),
13583 t = xy[1],
13584 r = xy[0] + w,
13585 b = xy[1] + h,
13586 l = xy[0];
13587
13588 if (getRegion) {
13589 return new Ext.util.Region(t, r, b, l);
13590 }
13591 else {
13592 return {
13593 left: l,
13594 top: t,
13595 width: w,
13596 height: h,
13597 right: r,
13598 bottom: b
13599 };
13600 }
13601 }
13602 });
13603
13604 //@tag dom,core
13605 //@define Ext.Element-all
13606 //@define Ext.Element-style
13607 //@require Ext.Element-position
13608
13609 /**
13610 * @class Ext.dom.Element
13611 */
13612
13613 Ext.dom.Element.addMembers({
13614 WIDTH: 'width',
13615 HEIGHT: 'height',
13616 MIN_WIDTH: 'min-width',
13617 MIN_HEIGHT: 'min-height',
13618 MAX_WIDTH: 'max-width',
13619 MAX_HEIGHT: 'max-height',
13620 TOP: 'top',
13621 RIGHT: 'right',
13622 BOTTOM: 'bottom',
13623 LEFT: 'left',
13624 /**
13625 * @property VISIBILITY
13626 * Visibility mode constant for use with {@link #setVisibilityMode}. Use `visibility` to hide element.
13627 */
13628 VISIBILITY: 1,
13629
13630 /**
13631 * @property DISPLAY
13632 * Visibility mode constant for use with {@link #setVisibilityMode}. Use `display` to hide element.
13633 */
13634 DISPLAY: 2,
13635
13636 /**
13637 * @property OFFSETS
13638 * Visibility mode constant for use with {@link #setVisibilityMode}. Use offsets to hide element.
13639 */
13640 OFFSETS: 3,
13641
13642 SEPARATOR: '-',
13643
13644 trimRe: /^\s+|\s+$/g,
13645 wordsRe: /\w/g,
13646 spacesRe: /\s+/,
13647 styleSplitRe: /\s*(?::|;)\s*/,
13648 transparentRe: /^(?:transparent|(?:rgba[(](?:\s*\d+\s*[,]){3}\s*0\s*[)]))$/i,
13649 classNameSplitRegex: /[\s]+/,
13650
13651 borders: {
13652 t: 'border-top-width',
13653 r: 'border-right-width',
13654 b: 'border-bottom-width',
13655 l: 'border-left-width'
13656 },
13657
13658 paddings: {
13659 t: 'padding-top',
13660 r: 'padding-right',
13661 b: 'padding-bottom',
13662 l: 'padding-left'
13663 },
13664
13665 margins: {
13666 t: 'margin-top',
13667 r: 'margin-right',
13668 b: 'margin-bottom',
13669 l: 'margin-left'
13670 },
13671
13672 /**
13673 * @property {String} defaultUnit
13674 * The default unit to append to CSS values where a unit isn't provided.
13675 */
13676 defaultUnit: "px",
13677
13678 isSynchronized: false,
13679
13680 /**
13681 * @private
13682 */
13683 synchronize: function() {
13684 var dom = this.dom,
13685 hasClassMap = {},
13686 className = dom.className,
13687 classList, i, ln, name;
13688
13689 if (className.length > 0) {
13690 classList = dom.className.split(this.classNameSplitRegex);
13691
13692 for (i = 0, ln = classList.length; i < ln; i++) {
13693 name = classList[i];
13694 hasClassMap[name] = true;
13695 }
13696 }
13697 else {
13698 classList = [];
13699 }
13700
13701 this.classList = classList;
13702
13703 this.hasClassMap = hasClassMap;
13704
13705 this.isSynchronized = true;
13706
13707 return this;
13708 },
13709
13710 /**
13711 * Adds the given CSS class(es) to this Element.
13712 * @param {String} names The CSS class(es) to add to this element.
13713 * @param {String} [prefix] (optional) Prefix to prepend to each class.
13714 * @param {String} [suffix] (optional) Suffix to append to each class.
13715 */
13716 addCls: function(names, prefix, suffix) {
13717 if (!names) {
13718 return this;
13719 }
13720
13721 if (!this.isSynchronized) {
13722 this.synchronize();
13723 }
13724
13725 var dom = this.dom,
13726 map = this.hasClassMap,
13727 classList = this.classList,
13728 SEPARATOR = this.SEPARATOR,
13729 i, ln, name;
13730
13731 prefix = prefix ? prefix + SEPARATOR : '';
13732 suffix = suffix ? SEPARATOR + suffix : '';
13733
13734 if (typeof names == 'string') {
13735 names = names.split(this.spacesRe);
13736 }
13737
13738 for (i = 0, ln = names.length; i < ln; i++) {
13739 name = prefix + names[i] + suffix;
13740
13741 if (!map[name]) {
13742 map[name] = true;
13743 classList.push(name);
13744 }
13745 }
13746
13747 dom.className = classList.join(' ');
13748
13749 return this;
13750 },
13751
13752 /**
13753 * Removes the given CSS class(es) from this Element.
13754 * @param {String} names The CSS class(es) to remove from this element.
13755 * @param {String} [prefix=''] Prefix to prepend to each class to be removed.
13756 * @param {String} [suffix=''] Suffix to append to each class to be removed.
13757 */
13758 removeCls: function(names, prefix, suffix) {
13759 if (!names) {
13760 return this;
13761 }
13762
13763 if (!this.isSynchronized) {
13764 this.synchronize();
13765 }
13766
13767 if (!suffix) {
13768 suffix = '';
13769 }
13770
13771 var dom = this.dom,
13772 map = this.hasClassMap,
13773 classList = this.classList,
13774 SEPARATOR = this.SEPARATOR,
13775 i, ln, name;
13776
13777 prefix = prefix ? prefix + SEPARATOR : '';
13778 suffix = suffix ? SEPARATOR + suffix : '';
13779
13780 if (typeof names == 'string') {
13781 names = names.split(this.spacesRe);
13782 }
13783
13784 for (i = 0, ln = names.length; i < ln; i++) {
13785 name = prefix + names[i] + suffix;
13786
13787 if (map[name]) {
13788 delete map[name];
13789 Ext.Array.remove(classList, name);
13790 }
13791 }
13792
13793 dom.className = classList.join(' ');
13794
13795 return this;
13796 },
13797
13798 /**
13799 * Replaces a CSS class on the element with another.
13800 * If the old name does not exist, the new name will simply be added.
13801 * @param {String} oldName The CSS class to replace.
13802 * @param {String} newName The replacement CSS class.
13803 * @param {String} [prefix=''] Prefix to prepend to each class to be replaced.
13804 * @param {String} [suffix=''] Suffix to append to each class to be replaced.
13805 * @return {Ext.dom.Element} this
13806 */
13807 replaceCls: function(oldName, newName, prefix, suffix) {
13808 if (!oldName && !newName) {
13809 return this;
13810 }
13811
13812 oldName = oldName || [];
13813 newName = newName || [];
13814
13815 if (!this.isSynchronized) {
13816 this.synchronize();
13817 }
13818
13819 if (!suffix) {
13820 suffix = '';
13821 }
13822
13823 var dom = this.dom,
13824 map = this.hasClassMap,
13825 classList = this.classList,
13826 SEPARATOR = this.SEPARATOR,
13827 i, ln, name;
13828
13829 prefix = prefix ? prefix + SEPARATOR : '';
13830 suffix = suffix ? SEPARATOR + suffix : '';
13831
13832 if (typeof oldName == 'string') {
13833 oldName = oldName.split(this.spacesRe);
13834 }
13835 if (typeof newName == 'string') {
13836 newName = newName.split(this.spacesRe);
13837 }
13838
13839 for (i = 0, ln = oldName.length; i < ln; i++) {
13840 name = prefix + oldName[i] + suffix;
13841
13842 if (map[name]) {
13843 delete map[name];
13844 Ext.Array.remove(classList, name);
13845 }
13846 }
13847
13848 for (i = 0, ln = newName.length; i < ln; i++) {
13849 name = prefix + newName[i] + suffix;
13850
13851 if (!map[name]) {
13852 map[name] = true;
13853 classList.push(name);
13854 }
13855 }
13856
13857 dom.className = classList.join(' ');
13858
13859 return this;
13860 },
13861
13862 /**
13863 * Checks if the specified CSS class exists on this element's DOM node.
13864 * @param {String} name The CSS class to check for.
13865 * @return {Boolean} `true` if the class exists, else `false`.
13866 */
13867 hasCls: function(name) {
13868 if (!this.isSynchronized) {
13869 this.synchronize();
13870 }
13871
13872 return this.hasClassMap.hasOwnProperty(name);
13873 },
13874
13875 /**
13876 * Sets the specified CSS class on this element's DOM node.
13877 * @param {String/Array} className The CSS class to set on this element.
13878 */
13879 setCls: function(className) {
13880 var map = this.hasClassMap,
13881 i, ln, name;
13882
13883 if (typeof className == 'string') {
13884 className = className.split(this.spacesRe);
13885 }
13886
13887 for (i = 0, ln = className.length; i < ln; i++) {
13888 name = className[i];
13889 if (!map[name]) {
13890 map[name] = true;
13891 }
13892 }
13893
13894 this.classList = className.slice();
13895 this.dom.className = className.join(' ');
13896 },
13897
13898 /**
13899 * Toggles the specified CSS class on this element (removes it if it already exists, otherwise adds it).
13900 * @param {String} className The CSS class to toggle.
13901 * @return {Ext.dom.Element} this
13902 */
13903 toggleCls: function(className, force){
13904 if (typeof force !== 'boolean') {
13905 force = !this.hasCls(className);
13906 }
13907
13908 return (force) ? this.addCls(className) : this.removeCls(className);
13909 },
13910
13911 /**
13912 * @private
13913 * @param {String} firstClass
13914 * @param {String} secondClass
13915 * @param {Boolean} flag
13916 * @param {String} prefix
13917 * @return {Mixed}
13918 */
13919 swapCls: function(firstClass, secondClass, flag, prefix) {
13920 if (flag === undefined) {
13921 flag = true;
13922 }
13923
13924 var addedClass = flag ? firstClass : secondClass,
13925 removedClass = flag ? secondClass : firstClass;
13926
13927 if (removedClass) {
13928 this.removeCls(prefix ? prefix + '-' + removedClass : removedClass);
13929 }
13930
13931 if (addedClass) {
13932 this.addCls(prefix ? prefix + '-' + addedClass : addedClass);
13933 }
13934
13935 return this;
13936 },
13937
13938 /**
13939 * Set the width of this Element.
13940 * @param {Number/String} width The new width.
13941 * @return {Ext.dom.Element} this
13942 */
13943 setWidth: function(width) {
13944 return this.setLengthValue(this.WIDTH, width);
13945 },
13946
13947 /**
13948 * Set the height of this Element.
13949 * @param {Number/String} height The new height.
13950 * @return {Ext.dom.Element} this
13951 */
13952 setHeight: function(height) {
13953 return this.setLengthValue(this.HEIGHT, height);
13954 },
13955
13956 /**
13957 * Set the size of this Element.
13958 *
13959 * @param {Number/String} width The new width. This may be one of:
13960 *
13961 * - A Number specifying the new width in this Element's {@link #defaultUnit}s (by default, pixels).
13962 * - A String used to set the CSS width style. Animation may **not** be used.
13963 * - A size object in the format `{width: widthValue, height: heightValue}`.
13964 *
13965 * @param {Number/String} height The new height. This may be one of:
13966 *
13967 * - A Number specifying the new height in this Element's {@link #defaultUnit}s (by default, pixels).
13968 * - A String used to set the CSS height style. Animation may **not** be used.
13969 * @return {Ext.dom.Element} this
13970 */
13971 setSize: function(width, height) {
13972 if (Ext.isObject(width)) {
13973 // in case of object from getSize()
13974 height = width.height;
13975 width = width.width;
13976 }
13977
13978 this.setWidth(width);
13979 this.setHeight(height);
13980
13981 return this;
13982 },
13983
13984 /**
13985 * Set the minimum width of this Element.
13986 * @param {Number/String} width The new minimum width.
13987 * @return {Ext.dom.Element} this
13988 */
13989 setMinWidth: function(width) {
13990 return this.setLengthValue(this.MIN_WIDTH, width);
13991 },
13992
13993 /**
13994 * Set the minimum height of this Element.
13995 * @param {Number/String} height The new minimum height.
13996 * @return {Ext.dom.Element} this
13997 */
13998 setMinHeight: function(height) {
13999 return this.setLengthValue(this.MIN_HEIGHT, height);
14000 },
14001
14002 /**
14003 * Set the maximum width of this Element.
14004 * @param {Number/String} width The new maximum width.
14005 * @return {Ext.dom.Element} this
14006 */
14007 setMaxWidth: function(width) {
14008 return this.setLengthValue(this.MAX_WIDTH, width);
14009 },
14010
14011 /**
14012 * Set the maximum height of this Element.
14013 * @param {Number/String} height The new maximum height.
14014 * @return {Ext.dom.Element} this
14015 */
14016 setMaxHeight: function(height) {
14017 return this.setLengthValue(this.MAX_HEIGHT, height);
14018 },
14019
14020 /**
14021 * Sets the element's top position directly using CSS style (instead of {@link #setY}).
14022 * @param {String} top The top CSS property value.
14023 * @return {Ext.dom.Element} this
14024 */
14025 setTop: function(top) {
14026 return this.setLengthValue(this.TOP, top);
14027 },
14028
14029 /**
14030 * Sets the element's CSS right style.
14031 * @param {String} right The right CSS property value.
14032 * @return {Ext.dom.Element} this
14033 */
14034 setRight: function(right) {
14035 return this.setLengthValue(this.RIGHT, right);
14036 },
14037
14038 /**
14039 * Sets the element's CSS bottom style.
14040 * @param {String} bottom The bottom CSS property value.
14041 * @return {Ext.dom.Element} this
14042 */
14043 setBottom: function(bottom) {
14044 return this.setLengthValue(this.BOTTOM, bottom);
14045 },
14046
14047 /**
14048 * Sets the element's left position directly using CSS style (instead of {@link #setX}).
14049 * @param {String} left The left CSS property value.
14050 * @return {Ext.dom.Element} this
14051 */
14052 setLeft: function(left) {
14053 return this.setLengthValue(this.LEFT, left);
14054 },
14055
14056 setMargin: function(margin) {
14057 var domStyle = this.dom.style;
14058
14059 if (margin || margin === 0) {
14060 margin = this.self.unitizeBox((margin === true) ? 5 : margin);
14061 domStyle.setProperty('margin', margin, 'important');
14062 }
14063 else {
14064 domStyle.removeProperty('margin-top');
14065 domStyle.removeProperty('margin-right');
14066 domStyle.removeProperty('margin-bottom');
14067 domStyle.removeProperty('margin-left');
14068 }
14069 },
14070
14071 setPadding: function(padding) {
14072 var domStyle = this.dom.style;
14073
14074 if (padding || padding === 0) {
14075 padding = this.self.unitizeBox((padding === true) ? 5 : padding);
14076 domStyle.setProperty('padding', padding, 'important');
14077 }
14078 else {
14079 domStyle.removeProperty('padding-top');
14080 domStyle.removeProperty('padding-right');
14081 domStyle.removeProperty('padding-bottom');
14082 domStyle.removeProperty('padding-left');
14083 }
14084 },
14085
14086 setBorder: function(border) {
14087 var domStyle = this.dom.style;
14088
14089 if (border || border === 0) {
14090 border = this.self.unitizeBox((border === true) ? 1 : border);
14091 domStyle.setProperty('border-width', border, 'important');
14092 }
14093 else {
14094 domStyle.removeProperty('border-top-width');
14095 domStyle.removeProperty('border-right-width');
14096 domStyle.removeProperty('border-bottom-width');
14097 domStyle.removeProperty('border-left-width');
14098 }
14099 },
14100
14101 setLengthValue: function(name, value) {
14102 var domStyle = this.dom.style;
14103
14104 if (value === null) {
14105 domStyle.removeProperty(name);
14106 return this;
14107 }
14108
14109 if (typeof value == 'number') {
14110 value = value + 'px';
14111 }
14112
14113 domStyle.setProperty(name, value, 'important');
14114 return this;
14115 },
14116
14117 /**
14118 * Sets the visibility of the element (see details). If the `visibilityMode` is set to `Element.DISPLAY`, it will use
14119 * the display property to hide the element, otherwise it uses visibility. The default is to hide and show using the `visibility` property.
14120 * @param {Boolean} visible Whether the element is visible.
14121 * @return {Ext.Element} this
14122 */
14123 setVisible: function(visible) {
14124 var mode = this.getVisibilityMode(),
14125 method = visible ? 'removeCls' : 'addCls';
14126
14127 switch (mode) {
14128 case this.VISIBILITY:
14129 this.removeCls(['x-hidden-display', 'x-hidden-offsets']);
14130 this[method]('x-hidden-visibility');
14131 break;
14132
14133 case this.DISPLAY:
14134 this.removeCls(['x-hidden-visibility', 'x-hidden-offsets']);
14135 this[method]('x-hidden-display');
14136 break;
14137
14138 case this.OFFSETS:
14139 this.removeCls(['x-hidden-visibility', 'x-hidden-display']);
14140 this[method]('x-hidden-offsets');
14141 break;
14142 }
14143
14144 return this;
14145 },
14146
14147 getVisibilityMode: function() {
14148 var dom = this.dom,
14149 mode = Ext.dom.Element.data(dom, 'visibilityMode');
14150
14151 if (mode === undefined) {
14152 Ext.dom.Element.data(dom, 'visibilityMode', mode = this.DISPLAY);
14153 }
14154
14155 return mode;
14156 },
14157
14158 /**
14159 * Use this to change the visibility mode between {@link #VISIBILITY}, {@link #DISPLAY} or {@link #OFFSETS}.
14160 */
14161 setVisibilityMode: function(mode) {
14162 this.self.data(this.dom, 'visibilityMode', mode);
14163
14164 return this;
14165 },
14166
14167 /**
14168 * Shows this element.
14169 * Uses display mode to determine whether to use "display" or "visibility". See {@link #setVisible}.
14170 */
14171 show: function() {
14172 var dom = this.dom;
14173 if (dom) {
14174 dom.style.removeProperty('display');
14175 }
14176 },
14177
14178 /**
14179 * Hides this element.
14180 * Uses display mode to determine whether to use "display" or "visibility". See {@link #setVisible}.
14181 */
14182 hide: function() {
14183 this.dom.style.setProperty('display', 'none', 'important');
14184 },
14185
14186 setVisibility: function(isVisible) {
14187 var domStyle = this.dom.style;
14188
14189 if (isVisible) {
14190 domStyle.removeProperty('visibility');
14191 }
14192 else {
14193 domStyle.setProperty('visibility', 'hidden', 'important');
14194 }
14195 },
14196
14197 /**
14198 * This shared object is keyed by style name (e.g., 'margin-left' or 'marginLeft'). The
14199 * values are objects with the following properties:
14200 *
14201 * * `name` (String) : The actual name to be presented to the DOM. This is typically the value
14202 * returned by {@link #normalize}.
14203 * * `get` (Function) : A hook function that will perform the get on this style. These
14204 * functions receive "(dom, el)" arguments. The `dom` parameter is the DOM Element
14205 * from which to get the style. The `el` argument (may be `null`) is the Ext.Element.
14206 * * `set` (Function) : A hook function that will perform the set on this style. These
14207 * functions receive "(dom, value, el)" arguments. The `dom` parameter is the DOM Element
14208 * from which to get this style. The `value` parameter is the new value for the style. The
14209 * `el` argument (may be `null`) is the Ext.Element.
14210 *
14211 * The `this` pointer is the object that contains `get` or `set`, which means that
14212 * `this.name` can be accessed if needed. The hook functions are both optional.
14213 * @private
14214 */
14215 styleHooks: {},
14216
14217 // @private
14218 addStyles: function(sides, styles) {
14219 var totalSize = 0,
14220 sidesArr = sides.match(this.wordsRe),
14221 i = 0,
14222 len = sidesArr.length,
14223 side, size;
14224 for (; i < len; i++) {
14225 side = sidesArr[i];
14226 size = side && parseInt(this.getStyle(styles[side]), 10);
14227 if (size) {
14228 totalSize += Math.abs(size);
14229 }
14230 }
14231 return totalSize;
14232 },
14233
14234 /**
14235 * Checks if the current value of a style is equal to a given value.
14236 * @param {String} style property whose value is returned.
14237 * @param {String} value to check against.
14238 * @return {Boolean} `true` for when the current value equals the given value.
14239 */
14240 isStyle: function(style, val) {
14241 return this.getStyle(style) == val;
14242 },
14243
14244 getStyleValue: function(name) {
14245 return this.dom.style.getPropertyValue(name);
14246 },
14247
14248 /**
14249 * Normalizes `currentStyle` and `computedStyle`.
14250 * @param {String} prop The style property whose value is returned.
14251 * @return {String} The current value of the style property for this element.
14252 */
14253 getStyle: function(prop) {
14254 var me = this,
14255 dom = me.dom,
14256 hook = me.styleHooks[prop],
14257 cs, result;
14258
14259 if (dom == document) {
14260 return null;
14261 }
14262 if (!hook) {
14263 me.styleHooks[prop] = hook = { name: Ext.dom.Element.normalize(prop) };
14264 }
14265 if (hook.get) {
14266 return hook.get(dom, me);
14267 }
14268
14269 cs = window.getComputedStyle(dom, '');
14270
14271 // why the dom.style lookup? It is not true that "style == computedStyle" as
14272 // well as the fact that 0/false are valid answers...
14273 result = (cs && cs[hook.name]); // || dom.style[hook.name];
14274
14275 // WebKit returns rgb values for transparent, how does this work n IE9+
14276 // if (!supportsTransparentColor && result == 'rgba(0, 0, 0, 0)') {
14277 // result = 'transparent';
14278 // }
14279
14280 return result;
14281 },
14282
14283 /**
14284 * Wrapper for setting style properties, also takes single object parameter of multiple styles.
14285 * @param {String/Object} property The style property to be set, or an object of multiple styles.
14286 * @param {String} [value] The value to apply to the given property, or `null` if an object was passed.
14287 * @return {Ext.dom.Element} this
14288 */
14289 setStyle: function(prop, value) {
14290 var me = this,
14291 dom = me.dom,
14292 hooks = me.styleHooks,
14293 style = dom.style,
14294 valueFrom = Ext.valueFrom,
14295 name, hook;
14296
14297 // we don't promote the 2-arg form to object-form to avoid the overhead...
14298 if (typeof prop == 'string') {
14299 hook = hooks[prop];
14300
14301 if (!hook) {
14302 hooks[prop] = hook = { name: Ext.dom.Element.normalize(prop) };
14303 }
14304 value = valueFrom(value, '');
14305
14306 if (hook.set) {
14307 hook.set(dom, value, me);
14308 } else {
14309 style[hook.name] = value;
14310 }
14311 }
14312 else {
14313 for (name in prop) {
14314 if (prop.hasOwnProperty(name)) {
14315 hook = hooks[name];
14316
14317 if (!hook) {
14318 hooks[name] = hook = { name: Ext.dom.Element.normalize(name) };
14319 }
14320
14321 value = valueFrom(prop[name], '');
14322
14323 if (hook.set) {
14324 hook.set(dom, value, me);
14325 }
14326 else {
14327 style[hook.name] = value;
14328 }
14329 }
14330 }
14331 }
14332
14333 return me;
14334 },
14335
14336 /**
14337 * Returns the offset height of the element.
14338 * @param {Boolean} [contentHeight] `true` to get the height minus borders and padding.
14339 * @return {Number} The element's height.
14340 */
14341 getHeight: function(contentHeight) {
14342 var dom = this.dom,
14343 height = contentHeight ? (dom.clientHeight - this.getPadding("tb")) : dom.offsetHeight;
14344 return height > 0 ? height : 0;
14345 },
14346
14347 /**
14348 * Returns the offset width of the element.
14349 * @param {Boolean} [contentWidth] `true` to get the width minus borders and padding.
14350 * @return {Number} The element's width.
14351 */
14352 getWidth: function(contentWidth) {
14353 var dom = this.dom,
14354 width = contentWidth ? (dom.clientWidth - this.getPadding("lr")) : dom.offsetWidth;
14355 return width > 0 ? width : 0;
14356 },
14357
14358 /**
14359 * Gets the width of the border(s) for the specified side(s)
14360 * @param {String} side Can be t, l, r, b or any combination of those to add multiple values. For example,
14361 * passing `'lr'` would get the border **l**eft width + the border **r**ight width.
14362 * @return {Number} The width of the sides passed added together
14363 */
14364 getBorderWidth: function(side) {
14365 return this.addStyles(side, this.borders);
14366 },
14367
14368 /**
14369 * Gets the width of the padding(s) for the specified side(s).
14370 * @param {String} side Can be t, l, r, b or any combination of those to add multiple values. For example,
14371 * passing `'lr'` would get the padding **l**eft + the padding **r**ight.
14372 * @return {Number} The padding of the sides passed added together.
14373 */
14374 getPadding: function(side) {
14375 return this.addStyles(side, this.paddings);
14376 },
14377
14378 /**
14379 * More flexible version of {@link #setStyle} for setting style properties.
14380 * @param {String/Object/Function} styles A style specification string, e.g. "width:100px", or object in the form `{width:"100px"}`, or
14381 * a function which returns such a specification.
14382 * @return {Ext.dom.Element} this
14383 */
14384 applyStyles: function(styles) {
14385 if (styles) {
14386 var dom = this.dom,
14387 styleType, i, len;
14388
14389 if (typeof styles == 'function') {
14390 styles = styles.call();
14391 }
14392 styleType = typeof styles;
14393 if (styleType == 'string') {
14394 styles = Ext.util.Format.trim(styles).split(this.styleSplitRe);
14395 for (i = 0, len = styles.length; i < len;) {
14396 dom.style[Ext.dom.Element.normalize(styles[i++])] = styles[i++];
14397 }
14398 }
14399 else if (styleType == 'object') {
14400 this.setStyle(styles);
14401 }
14402 }
14403 return this;
14404 },
14405
14406 /**
14407 * Returns the size of the element.
14408 * @param {Boolean} [contentSize] `true` to get the width/size minus borders and padding.
14409 * @return {Object} An object containing the element's size:
14410 * @return {Number} return.width
14411 * @return {Number} return.height
14412 */
14413 getSize: function(contentSize) {
14414 var dom = this.dom;
14415 return {
14416 width: Math.max(0, contentSize ? (dom.clientWidth - this.getPadding("lr")) : dom.offsetWidth),
14417 height: Math.max(0, contentSize ? (dom.clientHeight - this.getPadding("tb")) : dom.offsetHeight)
14418 };
14419 },
14420
14421 /**
14422 * Forces the browser to repaint this element.
14423 * @return {Ext.dom.Element} this
14424 */
14425 repaint: function() {
14426 var dom = this.dom;
14427 this.addCls(Ext.baseCSSPrefix + 'repaint');
14428 setTimeout(function() {
14429 Ext.fly(dom).removeCls(Ext.baseCSSPrefix + 'repaint');
14430 }, 1);
14431 return this;
14432 },
14433
14434 /**
14435 * Returns an object with properties top, left, right and bottom representing the margins of this element unless sides is passed,
14436 * then it returns the calculated width of the sides (see {@link #getPadding}).
14437 * @param {String} [sides] Any combination of 'l', 'r', 't', 'b' to get the sum of those sides.
14438 * @return {Object/Number}
14439 */
14440 getMargin: function(side) {
14441 var me = this,
14442 hash = {t: "top", l: "left", r: "right", b: "bottom"},
14443 o = {},
14444 key;
14445
14446 if (!side) {
14447 for (key in me.margins) {
14448 o[hash[key]] = parseFloat(me.getStyle(me.margins[key])) || 0;
14449 }
14450 return o;
14451 } else {
14452 return me.addStyles.call(me, side, me.margins);
14453 }
14454 },
14455
14456 translate: function() {
14457 var transformStyleName = 'webkitTransform' in document.createElement('div').style ? 'webkitTransform' : 'transform';
14458
14459 return function(x, y, z) {
14460 this.dom.style[transformStyleName] = 'translate3d(' + (x || 0) + 'px, ' + (y || 0) + 'px, ' + (z || 0) + 'px)';
14461 }
14462 }()
14463 });
14464
14465
14466 //@tag dom,core
14467 //@define Ext.Element-all
14468 //@define Ext.Element-traversal
14469 //@require Ext.Element-style
14470
14471 /**
14472 * @class Ext.dom.Element
14473 */
14474 Ext.dom.Element.addMembers({
14475 getParent: function() {
14476 return Ext.get(this.dom.parentNode);
14477 },
14478
14479 getFirstChild: function() {
14480 return Ext.get(this.dom.firstElementChild);
14481 },
14482
14483 /**
14484 * Returns `true` if this element is an ancestor of the passed element.
14485 * @param {HTMLElement/String} element The element to check.
14486 * @return {Boolean} `true` if this element is an ancestor of `el`, else `false`.
14487 */
14488 contains: function(element) {
14489 if (!element) {
14490 return false;
14491 }
14492
14493 var dom = Ext.getDom(element);
14494
14495 // we need el-contains-itself logic here because isAncestor does not do that:
14496 return (dom === this.dom) || this.self.isAncestor(this.dom, dom);
14497 },
14498
14499 /**
14500 * Looks at this node and then at parent nodes for a match of the passed simple selector (e.g. 'div.some-class' or 'span:first-child')
14501 * @param {String} simpleSelector The simple selector to test.
14502 * @param {Number/String/HTMLElement/Ext.Element} maxDepth (optional)
14503 * The max depth to search as a number or element (defaults to `50 || document.body`)
14504 * @param {Boolean} returnEl (optional) `true` to return a Ext.Element object instead of DOM node.
14505 * @return {HTMLElement/null} The matching DOM node (or `null` if no match was found).
14506 */
14507 findParent: function(simpleSelector, maxDepth, returnEl) {
14508 var p = this.dom,
14509 b = document.body,
14510 depth = 0,
14511 stopEl;
14512
14513 maxDepth = maxDepth || 50;
14514 if (isNaN(maxDepth)) {
14515 stopEl = Ext.getDom(maxDepth);
14516 maxDepth = Number.MAX_VALUE;
14517 }
14518 while (p && p.nodeType == 1 && depth < maxDepth && p != b && p != stopEl) {
14519 if (Ext.DomQuery.is(p, simpleSelector)) {
14520 return returnEl ? Ext.get(p) : p;
14521 }
14522 depth++;
14523 p = p.parentNode;
14524 }
14525 return null;
14526 },
14527
14528 /**
14529 * Looks at parent nodes for a match of the passed simple selector (e.g. 'div.some-class' or 'span:first-child').
14530 * @param {String} simpleSelector The simple selector to test.
14531 * @param {Number/String/HTMLElement/Ext.Element} maxDepth (optional)
14532 * The max depth to search as a number or element (defaults to `10 || document.body`).
14533 * @param {Boolean} returnEl (optional) `true` to return a Ext.Element object instead of DOM node.
14534 * @return {HTMLElement/null} The matching DOM node (or `null` if no match was found).
14535 */
14536 findParentNode: function(simpleSelector, maxDepth, returnEl) {
14537 var p = Ext.fly(this.dom.parentNode, '_internal');
14538 return p ? p.findParent(simpleSelector, maxDepth, returnEl) : null;
14539 },
14540
14541 /**
14542 * Walks up the dom looking for a parent node that matches the passed simple selector (e.g. 'div.some-class' or 'span:first-child').
14543 * This is a shortcut for `findParentNode()` that always returns an Ext.dom.Element.
14544 * @param {String} simpleSelector The simple selector to test
14545 * @param {Number/String/HTMLElement/Ext.Element} maxDepth (optional)
14546 * The max depth to search as a number or element (defaults to `10 || document.body`).
14547 * @return {Ext.dom.Element/null} The matching DOM node (or `null` if no match was found).
14548 */
14549 up: function(simpleSelector, maxDepth) {
14550 return this.findParentNode(simpleSelector, maxDepth, true);
14551 },
14552
14553 /**
14554 * Selects elements based on the passed CSS selector to enable {@link Ext.Element Element} methods
14555 * to be applied to many related elements in one statement through the returned. The element is the root of the search.
14556 * {@link Ext.dom.CompositeElementLite CompositeElementLite} object.
14557 * @param {String/HTMLElement[]} selector The CSS selector or an array of elements
14558 * @param {Boolean} composite Return a CompositeElement as opposed to a CompositeElementLite. Defaults to false.
14559 * @return {Ext.dom.CompositeElementLite/Ext.dom.CompositeElement}
14560 */
14561 select: function(selector, composite) {
14562 return Ext.dom.Element.select(selector, composite, this.dom);
14563 },
14564
14565 /**
14566 * Selects child nodes based on the passed CSS selector (the selector should not contain an id).
14567 * @param {String} selector The CSS selector.
14568 * @return {HTMLElement[]} An array of the matched nodes.
14569 */
14570 query: function(selector) {
14571 return Ext.DomQuery.select(selector, this.dom);
14572 },
14573
14574 /**
14575 * Selects a single child at any depth below this element based on the passed CSS selector (the selector should not contain an id).
14576 * @param {String} selector The CSS selector.
14577 * @param {Boolean} [returnDom=false] (optional) `true` to return the DOM node instead of Ext.dom.Element.
14578 * @return {HTMLElement/Ext.dom.Element} The child Ext.dom.Element (or DOM node if `returnDom` is `true`).
14579 */
14580 down: function(selector, returnDom) {
14581 var n = Ext.DomQuery.selectNode(selector, this.dom);
14582 return returnDom ? n : Ext.get(n);
14583 },
14584
14585 /**
14586 * Selects a single *direct* child based on the passed CSS selector (the selector should not contain an id).
14587 * @param {String} selector The CSS selector.
14588 * @param {Boolean} [returnDom=false] (optional) `true` to return the DOM node instead of Ext.dom.Element.
14589 * @return {HTMLElement/Ext.dom.Element} The child Ext.dom.Element (or DOM node if `returnDom` is `true`)
14590 */
14591 child: function(selector, returnDom) {
14592 var node,
14593 me = this,
14594 id;
14595 id = Ext.get(me).id;
14596 // Escape . or :
14597 id = id.replace(/[\.:]/g, "\\$0");
14598 node = Ext.DomQuery.selectNode('#' + id + " > " + selector, me.dom);
14599 return returnDom ? node : Ext.get(node);
14600 },
14601
14602 /**
14603 * Gets the parent node for this element, optionally chaining up trying to match a selector.
14604 * @param {String} selector (optional) Find a parent node that matches the passed simple selector.
14605 * @param {Boolean} returnDom (optional) `true` to return a raw DOM node instead of an Ext.dom.Element.
14606 * @return {Ext.dom.Element/HTMLElement/null} The parent node or `null`.
14607 */
14608 parent: function(selector, returnDom) {
14609 return this.matchNode('parentNode', 'parentNode', selector, returnDom);
14610 },
14611
14612 /**
14613 * Gets the next sibling, skipping text nodes.
14614 * @param {String} selector (optional) Find the next sibling that matches the passed simple selector.
14615 * @param {Boolean} returnDom (optional) `true` to return a raw dom node instead of an Ext.dom.Element.
14616 * @return {Ext.dom.Element/HTMLElement/null} The next sibling or `null`.
14617 */
14618 next: function(selector, returnDom) {
14619 return this.matchNode('nextSibling', 'nextSibling', selector, returnDom);
14620 },
14621
14622 /**
14623 * Gets the previous sibling, skipping text nodes.
14624 * @param {String} selector (optional) Find the previous sibling that matches the passed simple selector.
14625 * @param {Boolean} returnDom (optional) `true` to return a raw DOM node instead of an Ext.dom.Element
14626 * @return {Ext.dom.Element/HTMLElement/null} The previous sibling or `null`.
14627 */
14628 prev: function(selector, returnDom) {
14629 return this.matchNode('previousSibling', 'previousSibling', selector, returnDom);
14630 },
14631
14632
14633 /**
14634 * Gets the first child, skipping text nodes.
14635 * @param {String} selector (optional) Find the next sibling that matches the passed simple selector.
14636 * @param {Boolean} returnDom (optional) `true` to return a raw DOM node instead of an Ext.dom.Element.
14637 * @return {Ext.dom.Element/HTMLElement/null} The first child or `null`.
14638 */
14639 first: function(selector, returnDom) {
14640 return this.matchNode('nextSibling', 'firstChild', selector, returnDom);
14641 },
14642
14643 /**
14644 * Gets the last child, skipping text nodes.
14645 * @param {String} selector (optional) Find the previous sibling that matches the passed simple selector.
14646 * @param {Boolean} returnDom (optional) `true` to return a raw DOM node instead of an Ext.dom.Element.
14647 * @return {Ext.dom.Element/HTMLElement/null} The last child or `null`.
14648 */
14649 last: function(selector, returnDom) {
14650 return this.matchNode('previousSibling', 'lastChild', selector, returnDom);
14651 },
14652
14653 matchNode: function(dir, start, selector, returnDom) {
14654 if (!this.dom) {
14655 return null;
14656 }
14657
14658 var n = this.dom[start];
14659 while (n) {
14660 if (n.nodeType == 1 && (!selector || Ext.DomQuery.is(n, selector))) {
14661 return !returnDom ? Ext.get(n) : n;
14662 }
14663 n = n[dir];
14664 }
14665 return null;
14666 },
14667
14668 isAncestor: function(element) {
14669 return this.self.isAncestor.call(this.self, this.dom, element);
14670 }
14671 });
14672
14673 //@tag dom,core
14674 //@require Ext.Element-all
14675
14676 /**
14677 * This class encapsulates a *collection* of DOM elements, providing methods to filter members, or to perform collective
14678 * actions upon the whole set.
14679 *
14680 * Although they are not listed, this class supports all of the methods of {@link Ext.dom.Element} and
14681 * {@link Ext.Anim}. The methods from these classes will be performed on all the elements in this collection.
14682 *
14683 * Example:
14684 *
14685 * var els = Ext.select("#some-el div.some-class");
14686 * // or select directly from an existing element
14687 * var el = Ext.get('some-el');
14688 * el.select('div.some-class');
14689 *
14690 * els.setWidth(100); // all elements become 100 width
14691 * els.hide(true); // all elements fade out and hide
14692 * // or
14693 * els.setWidth(100).hide(true);
14694 *
14695 * @mixins Ext.dom.Element
14696 */
14697 Ext.define('Ext.dom.CompositeElementLite', {
14698 alternateClassName: ['Ext.CompositeElementLite', 'Ext.CompositeElement'],
14699
14700
14701
14702 // We use the @mixins tag above to document that CompositeElement has
14703 // all the same methods as Element, but the @mixins tag also pulls in
14704 // configs and properties which we don't want, so hide them explicitly:
14705 /** @cfg bubbleEvents @hide */
14706 /** @cfg listeners @hide */
14707 /** @property DISPLAY @hide */
14708 /** @property OFFSETS @hide */
14709 /** @property VISIBILITY @hide */
14710 /** @property defaultUnit @hide */
14711 /** @property dom @hide */
14712 /** @property id @hide */
14713 // Also hide the static #get method that also gets inherited
14714 /** @method get @static @hide */
14715
14716 statics: {
14717 /**
14718 * @private
14719 * @static
14720 * Copies all of the functions from Ext.dom.Element's prototype onto CompositeElementLite's prototype.
14721 */
14722 importElementMethods: function() {
14723
14724 }
14725 },
14726
14727 constructor: function(elements, root) {
14728 /**
14729 * @property {HTMLElement[]} elements
14730 * @readonly
14731 * The Array of DOM elements which this CompositeElement encapsulates.
14732 *
14733 * This will not *usually* be accessed in developers' code, but developers wishing to augment the capabilities
14734 * of the CompositeElementLite class may use it when adding methods to the class.
14735 *
14736 * For example to add the `nextAll` method to the class to **add** all following siblings of selected elements,
14737 * the code would be
14738 *
14739 * Ext.override(Ext.dom.CompositeElementLite, {
14740 * nextAll: function() {
14741 * var elements = this.elements, i, l = elements.length, n, r = [], ri = -1;
14742 *
14743 * // Loop through all elements in this Composite, accumulating
14744 * // an Array of all siblings.
14745 * for (i = 0; i < l; i++) {
14746 * for (n = elements[i].nextSibling; n; n = n.nextSibling) {
14747 * r[++ri] = n;
14748 * }
14749 * }
14750 *
14751 * // Add all found siblings to this Composite
14752 * return this.add(r);
14753 * }
14754 * });
14755 */
14756 this.elements = [];
14757 this.add(elements, root);
14758 this.el = new Ext.dom.Element.Fly();
14759 },
14760
14761 isComposite: true,
14762
14763 // @private
14764 getElement: function(el) {
14765 // Set the shared flyweight dom property to the current element
14766 return this.el.attach(el).synchronize();
14767 },
14768
14769 // @private
14770 transformElement: function(el) {
14771 return Ext.getDom(el);
14772 },
14773
14774 /**
14775 * Returns the number of elements in this Composite.
14776 * @return {Number}
14777 */
14778 getCount: function() {
14779 return this.elements.length;
14780 },
14781
14782 /**
14783 * Adds elements to this Composite object.
14784 * @param {HTMLElement[]/Ext.dom.CompositeElementLite} els Either an Array of DOM elements to add, or another Composite
14785 * object who's elements should be added.
14786 * @param {HTMLElement/String} [root] The root element of the query or id of the root.
14787 * @return {Ext.dom.CompositeElementLite} This Composite object.
14788 */
14789 add: function(els, root) {
14790 var elements = this.elements,
14791 i, ln;
14792
14793 if (!els) {
14794 return this;
14795 }
14796
14797 if (typeof els == "string") {
14798 els = Ext.dom.Element.selectorFunction(els, root);
14799 }
14800 else if (els.isComposite) {
14801 els = els.elements;
14802 }
14803 else if (!Ext.isIterable(els)) {
14804 els = [els];
14805 }
14806
14807 for (i = 0, ln = els.length; i < ln; ++i) {
14808 elements.push(this.transformElement(els[i]));
14809 }
14810
14811 return this;
14812 },
14813
14814 invoke: function(fn, args) {
14815 var elements = this.elements,
14816 ln = elements.length,
14817 element,
14818 i;
14819
14820 for (i = 0; i < ln; i++) {
14821 element = elements[i];
14822
14823 if (element) {
14824 Ext.dom.Element.prototype[fn].apply(this.getElement(element), args);
14825 }
14826 }
14827 return this;
14828 },
14829
14830 /**
14831 * Returns a flyweight Element of the dom element object at the specified index.
14832 * @param {Number} index
14833 * @return {Ext.dom.Element}
14834 */
14835 item: function(index) {
14836 var el = this.elements[index],
14837 out = null;
14838
14839 if (el) {
14840 out = this.getElement(el);
14841 }
14842
14843 return out;
14844 },
14845
14846 // fixes scope with flyweight.
14847 addListener: function(eventName, handler, scope, opt) {
14848 var els = this.elements,
14849 len = els.length,
14850 i, e;
14851
14852 for (i = 0; i < len; i++) {
14853 e = els[i];
14854 if (e) {
14855 e.on(eventName, handler, scope || e, opt);
14856 }
14857 }
14858 return this;
14859 },
14860 /**
14861 * Calls the passed function for each element in this composite.
14862 * @param {Function} fn The function to call.
14863 * @param {Ext.dom.Element} fn.el The current Element in the iteration. **This is the flyweight
14864 * (shared) Ext.dom.Element instance, so if you require a a reference to the dom node, use el.dom.**
14865 * @param {Ext.dom.CompositeElementLite} fn.c This Composite object.
14866 * @param {Number} fn.index The zero-based index in the iteration.
14867 * @param {Object} [scope] The scope (this reference) in which the function is executed.
14868 * Defaults to the Element.
14869 * @return {Ext.dom.CompositeElementLite} this
14870 */
14871 each: function(fn, scope) {
14872 var me = this,
14873 els = me.elements,
14874 len = els.length,
14875 i, e;
14876
14877 for (i = 0; i < len; i++) {
14878 e = els[i];
14879 if (e) {
14880 e = this.getElement(e);
14881 if (fn.call(scope || e, e, me, i) === false) {
14882 break;
14883 }
14884 }
14885 }
14886 return me;
14887 },
14888
14889 /**
14890 * Clears this Composite and adds the elements passed.
14891 * @param {HTMLElement[]/Ext.dom.CompositeElementLite} els Either an array of DOM elements, or another Composite from which
14892 * to fill this Composite.
14893 * @return {Ext.dom.CompositeElementLite} this
14894 */
14895 fill: function(els) {
14896 var me = this;
14897 me.elements = [];
14898 me.add(els);
14899 return me;
14900 },
14901
14902 /**
14903 * Filters this composite to only elements that match the passed selector.
14904 * @param {String/Function} selector A string CSS selector or a comparison function. The comparison function will be
14905 * called with the following arguments:
14906 * @param {Ext.dom.Element} selector.el The current DOM element.
14907 * @param {Number} selector.index The current index within the collection.
14908 * @return {Ext.dom.CompositeElementLite} this
14909 */
14910 filter: function(selector) {
14911 var els = [],
14912 me = this,
14913 fn = Ext.isFunction(selector) ? selector
14914 : function(el) {
14915 return el.is(selector);
14916 };
14917
14918 me.each(function(el, self, i) {
14919 if (fn(el, i) !== false) {
14920 els[els.length] = me.transformElement(el);
14921 }
14922 });
14923
14924 me.elements = els;
14925 return me;
14926 },
14927
14928 /**
14929 * Find the index of the passed element within the composite collection.
14930 * @param {String/HTMLElement/Ext.Element/Number} el The id of an element, or an Ext.dom.Element, or an HtmlElement
14931 * to find within the composite collection.
14932 * @return {Number} The index of the passed Ext.dom.Element in the composite collection, or -1 if not found.
14933 */
14934 indexOf: function(el) {
14935 return Ext.Array.indexOf(this.elements, this.transformElement(el));
14936 },
14937
14938 /**
14939 * Replaces the specified element with the passed element.
14940 * @param {String/HTMLElement/Ext.Element/Number} el The id of an element, the Element itself, the index of the
14941 * element in this composite to replace.
14942 * @param {String/Ext.Element} replacement The id of an element or the Element itself.
14943 * @param {Boolean} [domReplace] `true` to remove and replace the element in the document too.
14944 * @return {Ext.dom.CompositeElementLite} this
14945 */
14946 replaceElement: function(el, replacement, domReplace) {
14947 var index = !isNaN(el) ? el : this.indexOf(el),
14948 d;
14949 if (index > -1) {
14950 replacement = Ext.getDom(replacement);
14951 if (domReplace) {
14952 d = this.elements[index];
14953 d.parentNode.insertBefore(replacement, d);
14954 Ext.removeNode(d);
14955 }
14956 Ext.Array.splice(this.elements, index, 1, replacement);
14957 }
14958 return this;
14959 },
14960
14961 /**
14962 * Removes all elements.
14963 */
14964 clear: function() {
14965 this.elements = [];
14966 },
14967
14968 addElements: function(els, root) {
14969 if (!els) {
14970 return this;
14971 }
14972
14973 if (typeof els == "string") {
14974 els = Ext.dom.Element.selectorFunction(els, root);
14975 }
14976
14977 var yels = this.elements;
14978
14979 Ext.each(els, function(e) {
14980 yels.push(Ext.get(e));
14981 });
14982
14983 return this;
14984 },
14985
14986 /**
14987 * Returns the first Element
14988 * @return {Ext.dom.Element}
14989 */
14990 first: function() {
14991 return this.item(0);
14992 },
14993
14994 /**
14995 * Returns the last Element
14996 * @return {Ext.dom.Element}
14997 */
14998 last: function() {
14999 return this.item(this.getCount() - 1);
15000 },
15001
15002 /**
15003 * Returns `true` if this composite contains the passed element
15004 * @param {String/HTMLElement/Ext.Element/Number} el The id of an element, or an Ext.Element, or an HtmlElement to
15005 * find within the composite collection.
15006 * @return {Boolean}
15007 */
15008 contains: function(el) {
15009 return this.indexOf(el) != -1;
15010 },
15011
15012 /**
15013 * Removes the specified element(s).
15014 * @param {String/HTMLElement/Ext.Element/Number} el The id of an element, the Element itself, the index of the
15015 * element in this composite or an array of any of those.
15016 * @param {Boolean} [removeDom] `true` to also remove the element from the document
15017 * @return {Ext.dom.CompositeElementLite} this
15018 */
15019 removeElement: function(keys, removeDom) {
15020 var me = this,
15021 elements = this.elements,
15022 el;
15023
15024 Ext.each(keys, function(val) {
15025 if ((el = (elements[val] || elements[val = me.indexOf(val)]))) {
15026 if (removeDom) {
15027 if (el.dom) {
15028 el.remove();
15029 }
15030 else {
15031 Ext.removeNode(el);
15032 }
15033 }
15034 Ext.Array.erase(elements, val, 1);
15035 }
15036 });
15037
15038 return this;
15039 }
15040
15041 }, function() {
15042 var Element = Ext.dom.Element,
15043 elementPrototype = Element.prototype,
15044 prototype = this.prototype,
15045 name;
15046
15047 for (name in elementPrototype) {
15048 if (typeof elementPrototype[name] == 'function') {
15049 (function(key) {
15050 if (key === 'destroy') {
15051 prototype[key] = function() {
15052 return this.invoke(key, arguments);
15053 };
15054 } else {
15055 prototype[key] = prototype[key] || function() {
15056 return this.invoke(key, arguments);
15057 };
15058 }
15059 }).call(prototype, name);
15060 }
15061 }
15062
15063 prototype.on = prototype.addListener;
15064
15065 Element.selectorFunction = Ext.DomQuery.select;
15066
15067 /**
15068 * Selects elements based on the passed CSS selector to enable {@link Ext.Element Element} methods
15069 * to be applied to many related elements in one statement through the returned
15070 * {@link Ext.dom.CompositeElementLite CompositeElementLite} object.
15071 * @param {String/HTMLElement[]} selector The CSS selector or an array of elements
15072 * @param {Boolean} composite Return a CompositeElement as opposed to a CompositeElementLite. Defaults to false.
15073 * @param {HTMLElement/String} [root] The root element of the query or id of the root
15074 * @return {Ext.dom.CompositeElementLite/Ext.dom.CompositeElement}
15075 * @member Ext.dom.Element
15076 * @method select
15077 * @static
15078 */
15079 Ext.dom.Element.select = function(selector, composite, root) {
15080 var elements;
15081
15082 if (typeof selector == "string") {
15083 elements = Ext.dom.Element.selectorFunction(selector, root);
15084 }
15085 else if (selector.length !== undefined) {
15086 elements = selector;
15087 }
15088 else {
15089 //<debug>
15090 throw new Error("[Ext.select] Invalid selector specified: " + selector);
15091 //</debug>
15092 }
15093
15094 return (composite === true) ? new Ext.dom.CompositeElement(elements) : new Ext.dom.CompositeElementLite(elements);
15095 };
15096
15097 /**
15098 * @member Ext
15099 * @method select
15100 * @alias Ext.dom.Element#select
15101 */
15102 Ext.select = function() {
15103 return Element.select.apply(Element, arguments);
15104 };
15105 });
15106
15107 //@require @core
15108
15109 /**
15110 * @private
15111 */
15112 Ext.define('Ext.event.ListenerStack', {
15113
15114 currentOrder: 'current',
15115
15116 length: 0,
15117
15118 constructor: function() {
15119 this.listeners = {
15120 before: [],
15121 current: [],
15122 after: []
15123 };
15124
15125 this.lateBindingMap = {};
15126
15127 return this;
15128 },
15129
15130 add: function(fn, scope, options, order) {
15131 var lateBindingMap = this.lateBindingMap,
15132 listeners = this.getAll(order),
15133 i = listeners.length,
15134 bindingMap, listener, id;
15135
15136 if (typeof fn == 'string' && scope.isIdentifiable) {
15137 id = scope.getId();
15138
15139 bindingMap = lateBindingMap[id];
15140
15141 if (bindingMap) {
15142 if (bindingMap[fn]) {
15143 return false;
15144 }
15145 else {
15146 bindingMap[fn] = true;
15147 }
15148 }
15149 else {
15150 lateBindingMap[id] = bindingMap = {};
15151 bindingMap[fn] = true;
15152 }
15153 }
15154 else {
15155 if (i > 0) {
15156 while (i--) {
15157 listener = listeners[i];
15158
15159 if (listener.fn === fn && listener.scope === scope) {
15160 listener.options = options;
15161 return false;
15162 }
15163 }
15164 }
15165 }
15166
15167 listener = this.create(fn, scope, options, order);
15168
15169 if (options && options.prepend) {
15170 delete options.prepend;
15171 listeners.unshift(listener);
15172 }
15173 else {
15174 listeners.push(listener);
15175 }
15176
15177 this.length++;
15178
15179 return true;
15180 },
15181
15182 getAt: function(index, order) {
15183 return this.getAll(order)[index];
15184 },
15185
15186 getAll: function(order) {
15187 if (!order) {
15188 order = this.currentOrder;
15189 }
15190
15191 return this.listeners[order];
15192 },
15193
15194 count: function(order) {
15195 return this.getAll(order).length;
15196 },
15197
15198 create: function(fn, scope, options, order) {
15199 return {
15200 stack: this,
15201 fn: fn,
15202 firingFn: false,
15203 boundFn: false,
15204 isLateBinding: typeof fn == 'string',
15205 scope: scope,
15206 options: options || {},
15207 order: order
15208 };
15209 },
15210
15211 remove: function(fn, scope, order) {
15212 var listeners = this.getAll(order),
15213 i = listeners.length,
15214 isRemoved = false,
15215 lateBindingMap = this.lateBindingMap,
15216 listener, id;
15217
15218 if (i > 0) {
15219 // Start from the end index, faster than looping from the
15220 // beginning for "single" listeners,
15221 // which are normally LIFO
15222 while (i--) {
15223 listener = listeners[i];
15224
15225 if (listener.fn === fn && listener.scope === scope) {
15226 listeners.splice(i, 1);
15227 isRemoved = true;
15228 this.length--;
15229
15230 if (typeof fn == 'string' && scope.isIdentifiable) {
15231 id = scope.getId();
15232
15233 if (lateBindingMap[id] && lateBindingMap[id][fn]) {
15234 delete lateBindingMap[id][fn];
15235 }
15236 }
15237 break;
15238 }
15239 }
15240 }
15241
15242 return isRemoved;
15243 }
15244 });
15245
15246 //@require @core
15247
15248 /**
15249 * @private
15250 */
15251 Ext.define('Ext.event.Controller', {
15252
15253 isFiring: false,
15254
15255 listenerStack: null,
15256
15257 constructor: function(info) {
15258 this.firingListeners = [];
15259 this.firingArguments = [];
15260
15261 this.setInfo(info);
15262
15263 return this;
15264 },
15265
15266 setInfo: function(info) {
15267 this.info = info;
15268 },
15269
15270 getInfo: function() {
15271 return this.info;
15272 },
15273
15274 setListenerStacks: function(listenerStacks) {
15275 this.listenerStacks = listenerStacks;
15276 },
15277
15278 fire: function(args, action) {
15279 var listenerStacks = this.listenerStacks,
15280 firingListeners = this.firingListeners,
15281 firingArguments = this.firingArguments,
15282 push = firingListeners.push,
15283 ln = listenerStacks.length,
15284 listeners, beforeListeners, currentListeners, afterListeners,
15285 isActionBefore = false,
15286 isActionAfter = false,
15287 i;
15288
15289 firingListeners.length = 0;
15290
15291 if (action) {
15292 if (action.order !== 'after') {
15293 isActionBefore = true;
15294 }
15295 else {
15296 isActionAfter = true;
15297 }
15298 }
15299
15300 if (ln === 1) {
15301 listeners = listenerStacks[0].listeners;
15302 beforeListeners = listeners.before;
15303 currentListeners = listeners.current;
15304 afterListeners = listeners.after;
15305
15306 if (beforeListeners.length > 0) {
15307 push.apply(firingListeners, beforeListeners);
15308 }
15309
15310 if (isActionBefore) {
15311 push.call(firingListeners, action);
15312 }
15313
15314 if (currentListeners.length > 0) {
15315 push.apply(firingListeners, currentListeners);
15316 }
15317
15318 if (isActionAfter) {
15319 push.call(firingListeners, action);
15320 }
15321
15322 if (afterListeners.length > 0) {
15323 push.apply(firingListeners, afterListeners);
15324 }
15325 }
15326 else {
15327 for (i = 0; i < ln; i++) {
15328 beforeListeners = listenerStacks[i].listeners.before;
15329 if (beforeListeners.length > 0) {
15330 push.apply(firingListeners, beforeListeners);
15331 }
15332 }
15333
15334 if (isActionBefore) {
15335 push.call(firingListeners, action);
15336 }
15337
15338 for (i = 0; i < ln; i++) {
15339 currentListeners = listenerStacks[i].listeners.current;
15340 if (currentListeners.length > 0) {
15341 push.apply(firingListeners, currentListeners);
15342 }
15343 }
15344
15345 if (isActionAfter) {
15346 push.call(firingListeners, action);
15347 }
15348
15349 for (i = 0; i < ln; i++) {
15350 afterListeners = listenerStacks[i].listeners.after;
15351 if (afterListeners.length > 0) {
15352 push.apply(firingListeners, afterListeners);
15353 }
15354 }
15355 }
15356
15357 if (firingListeners.length === 0) {
15358 return this;
15359 }
15360
15361 if (!args) {
15362 args = [];
15363 }
15364
15365 firingArguments.length = 0;
15366 firingArguments.push.apply(firingArguments, args);
15367
15368 // Backwards compatibility
15369 firingArguments.push(null, this);
15370
15371 this.doFire();
15372
15373 return this;
15374 },
15375
15376 doFire: function() {
15377 var firingListeners = this.firingListeners,
15378 firingArguments = this.firingArguments,
15379 optionsArgumentIndex = firingArguments.length - 2,
15380 i, ln, listener, options, fn, firingFn,
15381 boundFn, isLateBinding, scope, args, result;
15382
15383 this.isPausing = false;
15384 this.isPaused = false;
15385 this.isStopped = false;
15386 this.isFiring = true;
15387
15388 for (i = 0,ln = firingListeners.length; i < ln; i++) {
15389 listener = firingListeners[i];
15390 options = listener.options;
15391 fn = listener.fn;
15392 firingFn = listener.firingFn;
15393 boundFn = listener.boundFn;
15394 isLateBinding = listener.isLateBinding;
15395 scope = listener.scope;
15396
15397 // Re-bind the callback if it has changed since the last time it's bound (overridden)
15398 if (isLateBinding && boundFn && boundFn !== scope[fn]) {
15399 boundFn = false;
15400 firingFn = false;
15401 }
15402
15403 if (!boundFn) {
15404 if (isLateBinding) {
15405 boundFn = scope[fn];
15406
15407 if (!boundFn) {
15408 continue;
15409 }
15410 }
15411 else {
15412 boundFn = fn;
15413 }
15414
15415 listener.boundFn = boundFn;
15416 }
15417
15418 if (!firingFn) {
15419 firingFn = boundFn;
15420
15421 if (options.buffer) {
15422 firingFn = Ext.Function.createBuffered(firingFn, options.buffer, scope);
15423 }
15424
15425 if (options.delay) {
15426 firingFn = Ext.Function.createDelayed(firingFn, options.delay, scope);
15427 }
15428
15429 listener.firingFn = firingFn;
15430 }
15431
15432 firingArguments[optionsArgumentIndex] = options;
15433
15434 args = firingArguments;
15435
15436 if (options.args) {
15437 args = options.args.concat(args);
15438 }
15439
15440 if (options.single === true) {
15441 listener.stack.remove(fn, scope, listener.order);
15442 }
15443
15444 result = firingFn.apply(scope, args);
15445
15446 if (result === false) {
15447 this.stop();
15448 }
15449
15450 if (this.isStopped) {
15451 break;
15452 }
15453
15454 if (this.isPausing) {
15455 this.isPaused = true;
15456 firingListeners.splice(0, i + 1);
15457 return;
15458 }
15459 }
15460
15461 this.isFiring = false;
15462 this.listenerStacks = null;
15463 firingListeners.length = 0;
15464 firingArguments.length = 0;
15465 this.connectingController = null;
15466 },
15467
15468 connect: function(controller) {
15469 this.connectingController = controller;
15470 },
15471
15472 resume: function() {
15473 var connectingController = this.connectingController;
15474
15475 this.isPausing = false;
15476
15477 if (this.isPaused && this.firingListeners.length > 0) {
15478 this.isPaused = false;
15479 this.doFire();
15480 }
15481
15482 if (connectingController) {
15483 connectingController.resume();
15484 }
15485
15486 return this;
15487 },
15488
15489 isInterrupted: function() {
15490 return this.isStopped || this.isPaused;
15491 },
15492
15493 stop: function() {
15494 var connectingController = this.connectingController;
15495
15496 this.isStopped = true;
15497
15498 if (connectingController) {
15499 this.connectingController = null;
15500 connectingController.stop();
15501 }
15502
15503 this.isFiring = false;
15504
15505 this.listenerStacks = null;
15506
15507 return this;
15508 },
15509
15510 pause: function() {
15511 var connectingController = this.connectingController;
15512
15513 this.isPausing = true;
15514
15515 if (connectingController) {
15516 connectingController.pause();
15517 }
15518
15519 return this;
15520 }
15521 });
15522
15523 //@require @core
15524
15525 /**
15526 * @private
15527 */
15528 Ext.define('Ext.event.Dispatcher', {
15529
15530
15531
15532
15533
15534
15535 statics: {
15536 getInstance: function() {
15537 if (!this.instance) {
15538 this.instance = new this();
15539 }
15540
15541 return this.instance;
15542 },
15543
15544 setInstance: function(instance) {
15545 this.instance = instance;
15546
15547 return this;
15548 }
15549 },
15550
15551 config: {
15552 publishers: {}
15553 },
15554
15555 wildcard: '*',
15556
15557 constructor: function(config) {
15558 this.listenerStacks = {};
15559
15560 this.activePublishers = {};
15561
15562 this.publishersCache = {};
15563
15564 this.noActivePublishers = [];
15565
15566 this.controller = null;
15567
15568 this.initConfig(config);
15569
15570 return this;
15571 },
15572
15573 getListenerStack: function(targetType, target, eventName, createIfNotExist) {
15574 var listenerStacks = this.listenerStacks,
15575 map = listenerStacks[targetType],
15576 listenerStack;
15577
15578 createIfNotExist = Boolean(createIfNotExist);
15579
15580 if (!map) {
15581 if (createIfNotExist) {
15582 listenerStacks[targetType] = map = {};
15583 }
15584 else {
15585 return null;
15586 }
15587 }
15588
15589 map = map[target];
15590
15591 if (!map) {
15592 if (createIfNotExist) {
15593 listenerStacks[targetType][target] = map = {};
15594 }
15595 else {
15596 return null;
15597 }
15598 }
15599
15600 listenerStack = map[eventName];
15601
15602 if (!listenerStack) {
15603 if (createIfNotExist) {
15604 map[eventName] = listenerStack = new Ext.event.ListenerStack();
15605 }
15606 else {
15607 return null;
15608 }
15609 }
15610
15611 return listenerStack;
15612 },
15613
15614 getController: function(targetType, target, eventName, connectedController) {
15615 var controller = this.controller,
15616 info = {
15617 targetType: targetType,
15618 target: target,
15619 eventName: eventName
15620 };
15621
15622 if (!controller) {
15623 this.controller = controller = new Ext.event.Controller();
15624 }
15625
15626 if (controller.isFiring) {
15627 controller = new Ext.event.Controller();
15628 }
15629
15630 controller.setInfo(info);
15631
15632 if (connectedController && controller !== connectedController) {
15633 controller.connect(connectedController);
15634 }
15635
15636 return controller;
15637 },
15638
15639 applyPublishers: function(publishers) {
15640 var i, publisher;
15641
15642 this.publishersCache = {};
15643
15644 for (i in publishers) {
15645 if (publishers.hasOwnProperty(i)) {
15646 publisher = publishers[i];
15647
15648 this.registerPublisher(publisher);
15649 }
15650 }
15651
15652 return publishers;
15653 },
15654
15655 registerPublisher: function(publisher) {
15656 var activePublishers = this.activePublishers,
15657 targetType = publisher.getTargetType(),
15658 publishers = activePublishers[targetType];
15659
15660 if (!publishers) {
15661 activePublishers[targetType] = publishers = [];
15662 }
15663
15664 publishers.push(publisher);
15665
15666 publisher.setDispatcher(this);
15667
15668 return this;
15669 },
15670
15671 getCachedActivePublishers: function(targetType, eventName) {
15672 var cache = this.publishersCache,
15673 publishers;
15674
15675 if ((publishers = cache[targetType]) && (publishers = publishers[eventName])) {
15676 return publishers;
15677 }
15678
15679 return null;
15680 },
15681
15682 cacheActivePublishers: function(targetType, eventName, publishers) {
15683 var cache = this.publishersCache;
15684
15685 if (!cache[targetType]) {
15686 cache[targetType] = {};
15687 }
15688
15689 cache[targetType][eventName] = publishers;
15690
15691 return publishers;
15692 },
15693
15694 getActivePublishers: function(targetType, eventName) {
15695 var publishers, activePublishers,
15696 i, ln, publisher;
15697
15698 if ((publishers = this.getCachedActivePublishers(targetType, eventName))) {
15699 return publishers;
15700 }
15701
15702 activePublishers = this.activePublishers[targetType];
15703
15704 if (activePublishers) {
15705 publishers = [];
15706
15707 for (i = 0,ln = activePublishers.length; i < ln; i++) {
15708 publisher = activePublishers[i];
15709
15710 if (publisher.handles(eventName)) {
15711 publishers.push(publisher);
15712 }
15713 }
15714 }
15715 else {
15716 publishers = this.noActivePublishers;
15717 }
15718
15719 return this.cacheActivePublishers(targetType, eventName, publishers);
15720 },
15721
15722 hasListener: function(targetType, target, eventName) {
15723 var listenerStack = this.getListenerStack(targetType, target, eventName);
15724
15725 if (listenerStack) {
15726 return listenerStack.count() > 0;
15727 }
15728
15729 return false;
15730 },
15731
15732 addListener: function(targetType, target, eventName) {
15733 var publishers = this.getActivePublishers(targetType, eventName),
15734 ln = publishers.length,
15735 i, result;
15736
15737 result = this.doAddListener.apply(this, arguments);
15738
15739 if (result && ln > 0) {
15740 for (i = 0; i < ln; i++) {
15741 publishers[i].subscribe(target, eventName);
15742 }
15743 }
15744
15745 return result;
15746 },
15747
15748 doAddListener: function(targetType, target, eventName, fn, scope, options, order) {
15749 var listenerStack = this.getListenerStack(targetType, target, eventName, true);
15750
15751 return listenerStack.add(fn, scope, options, order);
15752 },
15753
15754 removeListener: function(targetType, target, eventName) {
15755 var publishers = this.getActivePublishers(targetType, eventName),
15756 ln = publishers.length,
15757 i, result;
15758
15759 result = this.doRemoveListener.apply(this, arguments);
15760
15761 if (result && ln > 0) {
15762 for (i = 0; i < ln; i++) {
15763 publishers[i].unsubscribe(target, eventName);
15764 }
15765 }
15766
15767 return result;
15768 },
15769
15770 doRemoveListener: function(targetType, target, eventName, fn, scope, order) {
15771 var listenerStack = this.getListenerStack(targetType, target, eventName);
15772
15773 if (listenerStack === null) {
15774 return false;
15775 }
15776
15777 return listenerStack.remove(fn, scope, order);
15778 },
15779
15780 clearListeners: function(targetType, target, eventName) {
15781 var listenerStacks = this.listenerStacks,
15782 ln = arguments.length,
15783 stacks, publishers, i, publisherGroup;
15784
15785 if (ln === 3) {
15786 if (listenerStacks[targetType] && listenerStacks[targetType][target]) {
15787 this.removeListener(targetType, target, eventName);
15788 delete listenerStacks[targetType][target][eventName];
15789 }
15790 }
15791 else if (ln === 2) {
15792 if (listenerStacks[targetType]) {
15793 stacks = listenerStacks[targetType][target];
15794
15795 if (stacks) {
15796 for (eventName in stacks) {
15797 if (stacks.hasOwnProperty(eventName)) {
15798 publishers = this.getActivePublishers(targetType, eventName);
15799
15800 for (i = 0,ln = publishers.length; i < ln; i++) {
15801 publishers[i].unsubscribe(target, eventName, true);
15802 }
15803 }
15804 }
15805
15806 delete listenerStacks[targetType][target];
15807 }
15808 }
15809 }
15810 else if (ln === 1) {
15811 publishers = this.activePublishers[targetType];
15812
15813 for (i = 0,ln = publishers.length; i < ln; i++) {
15814 publishers[i].unsubscribeAll();
15815 }
15816
15817 delete listenerStacks[targetType];
15818 }
15819 else {
15820 publishers = this.activePublishers;
15821
15822 for (targetType in publishers) {
15823 if (publishers.hasOwnProperty(targetType)) {
15824 publisherGroup = publishers[targetType];
15825
15826 for (i = 0,ln = publisherGroup.length; i < ln; i++) {
15827 publisherGroup[i].unsubscribeAll();
15828 }
15829 }
15830 }
15831
15832 delete this.listenerStacks;
15833 this.listenerStacks = {};
15834 }
15835
15836 return this;
15837 },
15838
15839 dispatchEvent: function(targetType, target, eventName) {
15840 var publishers = this.getActivePublishers(targetType, eventName),
15841 ln = publishers.length,
15842 i;
15843
15844 if (ln > 0) {
15845 for (i = 0; i < ln; i++) {
15846 publishers[i].notify(target, eventName);
15847 }
15848 }
15849
15850 return this.doDispatchEvent.apply(this, arguments);
15851 },
15852
15853 doDispatchEvent: function(targetType, target, eventName, args, action, connectedController) {
15854 var listenerStack = this.getListenerStack(targetType, target, eventName),
15855 wildcardStacks = this.getWildcardListenerStacks(targetType, target, eventName),
15856 controller;
15857
15858 if ((listenerStack === null || listenerStack.length == 0)) {
15859 if (wildcardStacks.length == 0 && !action) {
15860 return;
15861 }
15862 }
15863 else {
15864 wildcardStacks.push(listenerStack);
15865 }
15866
15867 controller = this.getController(targetType, target, eventName, connectedController);
15868 controller.setListenerStacks(wildcardStacks);
15869 controller.fire(args, action);
15870
15871 return !controller.isInterrupted();
15872 },
15873
15874 getWildcardListenerStacks: function(targetType, target, eventName) {
15875 var stacks = [],
15876 wildcard = this.wildcard,
15877 isEventNameNotWildcard = eventName !== wildcard,
15878 isTargetNotWildcard = target !== wildcard,
15879 stack;
15880
15881 if (isEventNameNotWildcard && (stack = this.getListenerStack(targetType, target, wildcard))) {
15882 stacks.push(stack);
15883 }
15884
15885 if (isTargetNotWildcard && (stack = this.getListenerStack(targetType, wildcard, eventName))) {
15886 stacks.push(stack);
15887 }
15888
15889 return stacks;
15890 },
15891
15892 getPublisher: function (name) {
15893 return this.getPublishers()[name];
15894 }
15895 });
15896
15897 //@require Ext.Class
15898 //@require Ext.ClassManager
15899 //@require Ext.Loader
15900
15901 /**
15902 * Base class for all mixins.
15903 * @private
15904 */
15905 Ext.define('Ext.mixin.Mixin', {
15906 onClassExtended: function(cls, data) {
15907 var mixinConfig = data.mixinConfig,
15908 parentClassMixinConfig,
15909 beforeHooks, afterHooks;
15910
15911 if (mixinConfig) {
15912 parentClassMixinConfig = cls.superclass.mixinConfig;
15913
15914 if (parentClassMixinConfig) {
15915 mixinConfig = data.mixinConfig = Ext.merge({}, parentClassMixinConfig, mixinConfig);
15916 }
15917
15918 data.mixinId = mixinConfig.id;
15919
15920 beforeHooks = mixinConfig.beforeHooks;
15921 afterHooks = mixinConfig.hooks || mixinConfig.afterHooks;
15922
15923 if (beforeHooks || afterHooks) {
15924 Ext.Function.interceptBefore(data, 'onClassMixedIn', function(targetClass) {
15925 var mixin = this.prototype;
15926
15927 if (beforeHooks) {
15928 Ext.Object.each(beforeHooks, function(from, to) {
15929 targetClass.override(to, function() {
15930 if (mixin[from].apply(this, arguments) !== false) {
15931 return this.callOverridden(arguments);
15932 }
15933 });
15934 });
15935 }
15936
15937 if (afterHooks) {
15938 Ext.Object.each(afterHooks, function(from, to) {
15939 targetClass.override(to, function() {
15940 var ret = this.callOverridden(arguments);
15941
15942 mixin[from].apply(this, arguments);
15943
15944 return ret;
15945 });
15946 });
15947 }
15948 });
15949 }
15950 }
15951 }
15952 });
15953
15954 /**
15955 * Mixin that provides a common interface for publishing events. Classes using this mixin can use the {@link #fireEvent}
15956 * and {@link #fireAction} methods to notify listeners of events on the class.
15957 *
15958 * Classes can also define a {@link #listeners} config to add an event handler to the current object. See
15959 * {@link #addListener} for more details.
15960 *
15961 * ## Example
15962 *
15963 * Ext.define('Employee', {
15964 * mixins: ['Ext.mixin.Observable'],
15965 *
15966 * config: {
15967 * fullName: ''
15968 * },
15969 *
15970 * constructor: function(config) {
15971 * this.initConfig(config); // We need to initialize the config options when the class is instantiated
15972 * },
15973 *
15974 * quitJob: function() {
15975 * this.fireEvent('quit');
15976 * }
15977 * });
15978 *
15979 * var newEmployee = Ext.create('Employee', {
15980 *
15981 * fullName: 'Ed Spencer',
15982 *
15983 * listeners: {
15984 * quit: function() { // This function will be called when the 'quit' event is fired
15985 * // By default, "this" will be the object that fired the event.
15986 * console.log(this.getFullName() + " has quit!");
15987 * }
15988 * }
15989 * });
15990 *
15991 * newEmployee.quitJob(); // Will log 'Ed Spencer has quit!'
15992 *
15993 * For more information, please check out our [Events Guide](../../../core_concepts/events.html).
15994 */
15995 Ext.define('Ext.mixin.Observable', {
15996
15997
15998
15999 extend: Ext.mixin.Mixin ,
16000
16001 mixins: [ Ext.mixin.Identifiable ],
16002
16003 mixinConfig: {
16004 id: 'observable',
16005 hooks: {
16006 destroy: 'destroy'
16007 }
16008 },
16009
16010 alternateClassName: 'Ext.util.Observable',
16011
16012 // @private
16013 isObservable: true,
16014
16015 observableType: 'observable',
16016
16017 validIdRegex: /^([\w\-]+)$/,
16018
16019 observableIdPrefix: '#',
16020
16021 listenerOptionsRegex: /^(?:delegate|single|delay|buffer|args|prepend)$/,
16022
16023 eventFiringSuspended : false,
16024
16025 config: {
16026 /**
16027 * @cfg {Object} listeners
16028 *
16029 * A config object containing one or more event handlers to be added to this object during initialization. This
16030 * should be a valid listeners `config` object as specified in the {@link #addListener} example for attaching
16031 * multiple handlers at once.
16032 *
16033 * See the [Events Guide](../../../core_concepts/events.html) for more
16034 *
16035 * __Note:__ It is bad practice to specify a listener's `config` when you are defining a class using `Ext.define()`.
16036 * Instead, only specify listeners when you are instantiating your class with `Ext.create()`.
16037 * @accessor
16038 */
16039 listeners: null,
16040
16041 /**
16042 * @cfg {String/String[]} bubbleEvents The event name to bubble, or an Array of event names.
16043 * @accessor
16044 */
16045 bubbleEvents: null
16046 },
16047
16048 constructor: function(config) {
16049 this.initConfig(config);
16050 },
16051
16052 applyListeners: function(listeners) {
16053 if (listeners) {
16054 this.addListener(listeners);
16055 }
16056 },
16057
16058 applyBubbleEvents: function(bubbleEvents) {
16059 if (bubbleEvents) {
16060 this.enableBubble(bubbleEvents);
16061 }
16062 },
16063
16064 getOptimizedObservableId: function() {
16065 return this.observableId;
16066 },
16067
16068 getObservableId: function() {
16069 if (!this.observableId) {
16070 var id = this.getUniqueId();
16071
16072 //<debug error>
16073 if (!id.match(this.validIdRegex)) {
16074 Ext.Logger.error("Invalid unique id of '" + id + "' for this object", this);
16075 }
16076 //</debug>
16077
16078 this.observableId = this.observableIdPrefix + id;
16079
16080 this.getObservableId = this.getOptimizedObservableId;
16081 }
16082
16083 return this.observableId;
16084 },
16085
16086 getOptimizedEventDispatcher: function() {
16087 return this.eventDispatcher;
16088 },
16089
16090 getEventDispatcher: function() {
16091 if (!this.eventDispatcher) {
16092 this.eventDispatcher = Ext.event.Dispatcher.getInstance();
16093 this.getEventDispatcher = this.getOptimizedEventDispatcher;
16094
16095 this.getListeners();
16096 this.getBubbleEvents();
16097 }
16098
16099 return this.eventDispatcher;
16100 },
16101
16102 getManagedListeners: function(object, eventName) {
16103 var id = object.getUniqueId(),
16104 managedListeners = this.managedListeners;
16105
16106 if (!managedListeners) {
16107 this.managedListeners = managedListeners = {};
16108 }
16109
16110 if (!managedListeners[id]) {
16111 managedListeners[id] = {};
16112 object.doAddListener('destroy', 'clearManagedListeners', this, {
16113 single: true,
16114 args: [object]
16115 });
16116 }
16117
16118 if (!managedListeners[id][eventName]) {
16119 managedListeners[id][eventName] = [];
16120 }
16121
16122 return managedListeners[id][eventName];
16123 },
16124
16125 getUsedSelectors: function() {
16126 var selectors = this.usedSelectors;
16127
16128 if (!selectors) {
16129 selectors = this.usedSelectors = [];
16130 selectors.$map = {};
16131 }
16132
16133 return selectors;
16134 },
16135
16136 /**
16137 * Fires the specified event with the passed parameters (minus the event name, plus the `options` object passed
16138 * to {@link #addListener}).
16139 *
16140 * The first argument is the name of the event. Every other argument passed will be available when you listen for
16141 * the event.
16142 *
16143 * ## Example
16144 *
16145 * Firstly, we set up a listener for our new event.
16146 *
16147 * this.on('myevent', function(arg1, arg2, arg3, arg4, options, e) {
16148 * console.log(arg1); // true
16149 * console.log(arg2); // 2
16150 * console.log(arg3); // { test: 'foo' }
16151 * console.log(arg4); // 14
16152 * console.log(options); // the options added when adding the listener
16153 * console.log(e); // the event object with information about the event
16154 * });
16155 *
16156 * And then we can fire off the event.
16157 *
16158 * this.fireEvent('myevent', true, 2, { test: 'foo' }, 14);
16159 *
16160 * An event may be set to bubble up an Observable parent hierarchy by calling {@link #enableBubble}.
16161 *
16162 * @param {String} eventName The name of the event to fire.
16163 * @param {Object...} args Variable number of parameters are passed to handlers.
16164 * @return {Boolean} Returns `false` if any of the handlers return `false`.
16165 */
16166 fireEvent: function(eventName) {
16167 var args = Array.prototype.slice.call(arguments, 1);
16168
16169 return this.doFireEvent(eventName, args);
16170 },
16171
16172 /**
16173 * Fires the specified event with the passed parameters and execute a function (action)
16174 * at the end if there are no listeners that return `false`.
16175 *
16176 * @param {String} eventName The name of the event to fire.
16177 * @param {Array} args Arguments to pass to handers.
16178 * @param {Function} fn Action.
16179 * @param {Object} scope Scope of fn.
16180 * @return {Object}
16181 */
16182 fireAction: function(eventName, args, fn, scope, options, order) {
16183 var fnType = typeof fn,
16184 action;
16185
16186 if (args === undefined) {
16187 args = [];
16188 }
16189
16190 if (fnType != 'undefined') {
16191 action = {
16192 fn: fn,
16193 isLateBinding: fnType == 'string',
16194 scope: scope || this,
16195 options: options || {},
16196 order: order
16197 };
16198 }
16199
16200 return this.doFireEvent(eventName, args, action);
16201 },
16202
16203 doFireEvent: function(eventName, args, action, connectedController) {
16204 var me = this,
16205 ret = true,
16206 eventQueue;
16207
16208 if (me.eventFiringSuspended) {
16209 eventQueue = me.eventQueue;
16210 if (!eventQueue) {
16211 me.eventQueue = eventQueue = [];
16212 }
16213 eventQueue.push([eventName, args, action, connectedController]);
16214 } else {
16215 ret = me.getEventDispatcher().dispatchEvent(me.observableType, me.getObservableId(), eventName, args, action, connectedController);
16216 }
16217
16218 return ret;
16219 },
16220
16221 /**
16222 * @private
16223 * @return {Boolean}
16224 */
16225 doAddListener: function(name, fn, scope, options, order) {
16226 var isManaged = (scope && scope !== this && scope.isIdentifiable),
16227 usedSelectors = this.getUsedSelectors(),
16228 usedSelectorsMap = usedSelectors.$map,
16229 selector = this.getObservableId(),
16230 isAdded, managedListeners, delegate;
16231
16232 if (!options) {
16233 options = {};
16234 }
16235
16236 if (!scope) {
16237 scope = this;
16238 }
16239
16240 if (options.delegate) {
16241 delegate = options.delegate;
16242 // See https://sencha.jira.com/browse/TOUCH-1579
16243 selector += ' ' + delegate;
16244 }
16245
16246 if (!(selector in usedSelectorsMap)) {
16247 usedSelectorsMap[selector] = true;
16248 usedSelectors.push(selector);
16249 }
16250
16251 isAdded = this.addDispatcherListener(selector, name, fn, scope, options, order);
16252
16253 if (isAdded && isManaged) {
16254 managedListeners = this.getManagedListeners(scope, name);
16255 managedListeners.push({
16256 delegate: delegate,
16257 scope: scope,
16258 fn: fn,
16259 order: order
16260 });
16261 }
16262
16263 return isAdded;
16264 },
16265
16266 addDispatcherListener: function(selector, name, fn, scope, options, order) {
16267 return this.getEventDispatcher().addListener(this.observableType, selector, name, fn, scope, options, order);
16268 },
16269
16270 doRemoveListener: function(name, fn, scope, options, order) {
16271 var isManaged = (scope && scope !== this && scope.isIdentifiable),
16272 selector = this.getObservableId(),
16273 isRemoved,
16274 managedListeners, i, ln, listener, delegate;
16275
16276 if (options && options.delegate) {
16277 delegate = options.delegate;
16278 // See https://sencha.jira.com/browse/TOUCH-1579
16279 selector += ' ' + delegate;
16280 }
16281
16282 if (!scope) {
16283 scope = this;
16284 }
16285
16286 isRemoved = this.removeDispatcherListener(selector, name, fn, scope, order);
16287
16288 if (isRemoved && isManaged) {
16289 managedListeners = this.getManagedListeners(scope, name);
16290
16291 for (i = 0,ln = managedListeners.length; i < ln; i++) {
16292 listener = managedListeners[i];
16293
16294 if (listener.fn === fn && listener.scope === scope && listener.delegate === delegate && listener.order === order) {
16295 managedListeners.splice(i, 1);
16296 break;
16297 }
16298 }
16299 }
16300
16301 return isRemoved;
16302 },
16303
16304 removeDispatcherListener: function(selector, name, fn, scope, order) {
16305 return this.getEventDispatcher().removeListener(this.observableType, selector, name, fn, scope, order);
16306 },
16307
16308 clearManagedListeners: function(object) {
16309 var managedListeners = this.managedListeners,
16310 id, namedListeners, listeners, eventName, i, ln, listener, options;
16311
16312 if (!managedListeners) {
16313 return this;
16314 }
16315
16316 if (object) {
16317 if (typeof object != 'string') {
16318 id = object.getUniqueId();
16319 }
16320 else {
16321 id = object;
16322 }
16323
16324 namedListeners = managedListeners[id];
16325
16326 for (eventName in namedListeners) {
16327 if (namedListeners.hasOwnProperty(eventName)) {
16328 listeners = namedListeners[eventName];
16329
16330 for (i = 0,ln = listeners.length; i < ln; i++) {
16331 listener = listeners[i];
16332
16333 options = {};
16334
16335 if (listener.delegate) {
16336 options.delegate = listener.delegate;
16337 }
16338
16339 if (this.doRemoveListener(eventName, listener.fn, listener.scope, options, listener.order)) {
16340 i--;
16341 ln--;
16342 }
16343 }
16344 }
16345 }
16346
16347 delete managedListeners[id];
16348 return this;
16349 }
16350
16351 for (id in managedListeners) {
16352 if (managedListeners.hasOwnProperty(id)) {
16353 this.clearManagedListeners(id);
16354 }
16355 }
16356 },
16357
16358 /**
16359 * @private
16360 */
16361 changeListener: function(actionFn, eventName, fn, scope, options, order) {
16362 var eventNames,
16363 listeners,
16364 listenerOptionsRegex,
16365 actualOptions,
16366 name, value, i, ln, listener, valueType;
16367
16368 if (typeof fn != 'undefined') {
16369 // Support for array format to add multiple listeners
16370 if (typeof eventName != 'string') {
16371 for (i = 0,ln = eventName.length; i < ln; i++) {
16372 name = eventName[i];
16373
16374 actionFn.call(this, name, fn, scope, options, order);
16375 }
16376
16377 return this;
16378 }
16379
16380 actionFn.call(this, eventName, fn, scope, options, order);
16381 }
16382 else if (Ext.isArray(eventName)) {
16383 listeners = eventName;
16384
16385 for (i = 0,ln = listeners.length; i < ln; i++) {
16386 listener = listeners[i];
16387
16388 actionFn.call(this, listener.event, listener.fn, listener.scope, listener, listener.order);
16389 }
16390 }
16391 else {
16392 listenerOptionsRegex = this.listenerOptionsRegex;
16393 options = eventName;
16394 eventNames = [];
16395 listeners = [];
16396 actualOptions = {};
16397
16398 for (name in options) {
16399 value = options[name];
16400
16401 if (name === 'scope') {
16402 scope = value;
16403 continue;
16404 }
16405 else if (name === 'order') {
16406 order = value;
16407 continue;
16408 }
16409
16410 if (!listenerOptionsRegex.test(name)) {
16411 valueType = typeof value;
16412
16413 if (valueType != 'string' && valueType != 'function') {
16414 actionFn.call(this, name, value.fn, value.scope || scope, value, value.order || order);
16415 continue;
16416 }
16417
16418 eventNames.push(name);
16419 listeners.push(value);
16420 }
16421 else {
16422 actualOptions[name] = value;
16423 }
16424 }
16425
16426 for (i = 0,ln = eventNames.length; i < ln; i++) {
16427 actionFn.call(this, eventNames[i], listeners[i], scope, actualOptions, order);
16428 }
16429 }
16430
16431 return this;
16432 },
16433
16434 /**
16435 * Appends an event handler to this object. You can review the available handlers by looking at the 'events'
16436 * section of the documentation for the component you are working with.
16437 *
16438 * ## Combining Options
16439 *
16440 * Using the options argument, it is possible to combine different types of listeners:
16441 *
16442 * A delayed, one-time listener:
16443 *
16444 * container.addListener('tap', this.handleTap, this, {
16445 * single: true,
16446 * delay: 100
16447 * });
16448 *
16449 * ## Attaching multiple handlers in 1 call
16450 *
16451 * The method also allows for a single argument to be passed which is a config object containing properties which
16452 * specify multiple events. For example:
16453 *
16454 * container.addListener({
16455 * tap : this.onTap,
16456 * swipe: this.onSwipe,
16457 *
16458 * scope: this // Important. Ensure "this" is correct during handler execution
16459 * });
16460 *
16461 * One can also specify options for each event handler separately:
16462 *
16463 * container.addListener({
16464 * tap : { fn: this.onTap, scope: this, single: true },
16465 * swipe: { fn: button.onSwipe, scope: button }
16466 * });
16467 *
16468 * See the [Events Guide](../../../core_concepts/events.html) for more.
16469 *
16470 * @param {String/String[]/Object} eventName The name of the event to listen for. May also be an object who's property names are
16471 * event names.
16472 * @param {Function/String} [fn] The method the event invokes. Will be called with arguments given to
16473 * {@link #fireEvent} plus the `options` parameter described below.
16474 * @param {Object} [scope] The scope (`this` reference) in which the handler function is executed. **If
16475 * omitted, defaults to the object which fired the event.**
16476 * @param {Object} [options] An object containing handler configuration.
16477 *
16478 * This object may contain any of the following properties:
16479
16480 * @param {Object} [options.scope] The scope (`this` reference) in which the handler function is executed. If omitted, defaults to the object
16481 * which fired the event.
16482 * @param {Number} [options.delay] The number of milliseconds to delay the invocation of the handler after the event fires.
16483 * @param {Boolean} [options.single] `true` to add a handler to handle just the next firing of the event, and then remove itself.
16484 * @param {String} [options.order=current] The order of when the listener should be added into the listener queue.
16485 *
16486 * If you set an order of `before` and the event you are listening to is preventable, you can return `false` and it will stop the event.
16487 *
16488 * Available options are `before`, `current` and `after`.
16489 *
16490 * @param {Number} [options.buffer] Causes the handler to be delayed by the specified number of milliseconds. If the event fires again within that
16491 * time, the original handler is _not_ invoked, but the new handler is scheduled in its place.
16492 * @param {String} [options.element] Allows you to add a listener onto a element of this component using the elements reference.
16493 *
16494 * Ext.create('Ext.Component', {
16495 * listeners: {
16496 * element: 'element',
16497 * tap: function() {
16498 * alert('element tap!');
16499 * }
16500 * }
16501 * });
16502 *
16503 * All components have the `element` reference, which is the outer most element of the component. {@link Ext.Container} also has the
16504 * `innerElement` element which contains all children. In most cases `element` is adequate.
16505 *
16506 * @param {String} [options.delegate] Uses {@link Ext.ComponentQuery} to delegate events to a specified query selector within this item.
16507 *
16508 * // Create a container with a two children; a button and a toolbar
16509 * var container = Ext.create('Ext.Container', {
16510 * items: [
16511 * {
16512 * xtype: 'toolbar',
16513 * docked: 'top',
16514 * title: 'My Toolbar'
16515 * },
16516 * {
16517 * xtype: 'button',
16518 * text: 'My Button'
16519 * }
16520 * ]
16521 * });
16522 *
16523 * container.addListener({
16524 * // Ext.Buttons have an xtype of 'button', so we use that are a selector for our delegate
16525 * delegate: 'button',
16526 *
16527 * tap: function() {
16528 * alert('Button tapped!');
16529 * }
16530 * });
16531 *
16532 * @param {String} [order='current'] The order of when the listener should be added into the listener queue.
16533 * Possible values are `before`, `current` and `after`.
16534 */
16535 addListener: function(eventName, fn, scope, options, order) {
16536 return this.changeListener(this.doAddListener, eventName, fn, scope, options, order);
16537 },
16538
16539 toggleListener: function(toggle, eventName, fn, scope, options, order) {
16540 return this.changeListener(toggle ? this.doAddListener : this.doRemoveListener, eventName, fn, scope, options, order);
16541 },
16542
16543 /**
16544 * Appends a before-event handler. Returning `false` from the handler will stop the event.
16545 *
16546 * Same as {@link #addListener} with `order` set to `'before'`.
16547 *
16548 * @param {String/String[]/Object} eventName The name of the event to listen for.
16549 * @param {Function/String} fn The method the event invokes.
16550 * @param {Object} [scope] The scope for `fn`.
16551 * @param {Object} [options] An object containing handler configuration.
16552 */
16553 addBeforeListener: function(eventName, fn, scope, options) {
16554 return this.addListener(eventName, fn, scope, options, 'before');
16555 },
16556
16557 /**
16558 * Appends an after-event handler.
16559 *
16560 * Same as {@link #addListener} with `order` set to `'after'`.
16561 *
16562 * @param {String/String[]/Object} eventName The name of the event to listen for.
16563 * @param {Function/String} fn The method the event invokes.
16564 * @param {Object} [scope] The scope for `fn`.
16565 * @param {Object} [options] An object containing handler configuration.
16566 */
16567 addAfterListener: function(eventName, fn, scope, options) {
16568 return this.addListener(eventName, fn, scope, options, 'after');
16569 },
16570
16571 /**
16572 * Removes an event handler.
16573 *
16574 * @param {String/String[]/Object} eventName The type of event the handler was associated with.
16575 * @param {Function/String} fn The handler to remove. **This must be a reference to the function passed into the
16576 * {@link #addListener} call.**
16577 * @param {Object} [scope] The scope originally specified for the handler. It must be the same as the
16578 * scope argument specified in the original call to {@link #addListener} or the listener will not be removed.
16579 * @param {Object} [options] Extra options object. See {@link #addListener} for details.
16580 * @param {String} [order='current'] The order of the listener to remove.
16581 * Possible values are `before`, `current` and `after`.
16582 */
16583 removeListener: function(eventName, fn, scope, options, order) {
16584 return this.changeListener(this.doRemoveListener, eventName, fn, scope, options, order);
16585 },
16586
16587 /**
16588 * Removes a before-event handler.
16589 *
16590 * Same as {@link #removeListener} with `order` set to `'before'`.
16591 *
16592 * @param {String/String[]/Object} eventName The name of the event the handler was associated with.
16593 * @param {Function/String} fn The handler to remove.
16594 * @param {Object} [scope] The scope originally specified for `fn`.
16595 * @param {Object} [options] Extra options object.
16596 */
16597 removeBeforeListener: function(eventName, fn, scope, options) {
16598 return this.removeListener(eventName, fn, scope, options, 'before');
16599 },
16600
16601 /**
16602 * Removes a before-event handler.
16603 *
16604 * Same as {@link #removeListener} with `order` set to `'after'`.
16605 *
16606 * @param {String/String[]/Object} eventName The name of the event the handler was associated with.
16607 * @param {Function/String} fn The handler to remove.
16608 * @param {Object} [scope] The scope originally specified for `fn`.
16609 * @param {Object} [options] Extra options object.
16610 */
16611 removeAfterListener: function(eventName, fn, scope, options) {
16612 return this.removeListener(eventName, fn, scope, options, 'after');
16613 },
16614
16615 /**
16616 * Removes all listeners for this object.
16617 */
16618 clearListeners: function() {
16619 var usedSelectors = this.getUsedSelectors(),
16620 dispatcher = this.getEventDispatcher(),
16621 i, ln, selector;
16622
16623 for (i = 0,ln = usedSelectors.length; i < ln; i++) {
16624 selector = usedSelectors[i];
16625
16626 dispatcher.clearListeners(this.observableType, selector);
16627 }
16628 },
16629
16630 /**
16631 * Checks to see if this object has any listeners for a specified event
16632 *
16633 * @param {String} eventName The name of the event to check for
16634 * @return {Boolean} True if the event is being listened for, else false
16635 */
16636 hasListener: function(eventName) {
16637 return this.getEventDispatcher().hasListener(this.observableType, this.getObservableId(), eventName);
16638 },
16639
16640 /**
16641 * Suspends the firing of all events.
16642 *
16643 * All events will be queued but you can discard the queued events by passing false in
16644 * the {@link #resumeEvents} call
16645 */
16646 suspendEvents: function() {
16647 this.eventFiringSuspended = true;
16648 },
16649
16650 /**
16651 * Resumes firing events (see {@link #suspendEvents}).
16652 *
16653 * @param {Boolean} discardQueuedEvents Pass as true to discard any queued events.
16654 */
16655 resumeEvents: function(discardQueuedEvents) {
16656 var me = this,
16657 eventQueue = me.eventQueue || [],
16658 i, ln;
16659
16660 //resume the events
16661 me.eventFiringSuspended = false;
16662
16663 //don't loop over the queue if specified to discard the queue
16664 if (!discardQueuedEvents) {
16665 for (i = 0, ln = eventQueue.length; i < ln; i++) {
16666 me.doFireEvent.apply(me, eventQueue[i]);
16667 }
16668 }
16669
16670 //clear the queue
16671 me.eventQueue = [];
16672 },
16673
16674 /**
16675 * Relays selected events from the specified Observable as if the events were fired by `this`.
16676 * @param {Object} object The Observable whose events this object is to relay.
16677 * @param {String/Array/Object} events Array of event names to relay.
16678 */
16679 relayEvents: function(object, events, prefix) {
16680 var i, ln, oldName, newName;
16681
16682 if (typeof prefix == 'undefined') {
16683 prefix = '';
16684 }
16685
16686 if (typeof events == 'string') {
16687 events = [events];
16688 }
16689
16690 if (Ext.isArray(events)) {
16691 for (i = 0,ln = events.length; i < ln; i++) {
16692 oldName = events[i];
16693 newName = prefix + oldName;
16694
16695 object.addListener(oldName, this.createEventRelayer(newName), this);
16696 }
16697 }
16698 else {
16699 for (oldName in events) {
16700 if (events.hasOwnProperty(oldName)) {
16701 newName = prefix + events[oldName];
16702
16703 object.addListener(oldName, this.createEventRelayer(newName), this);
16704 }
16705 }
16706 }
16707
16708 return this;
16709 },
16710
16711 /**
16712 * @private
16713 */
16714 relayEvent: function(args, fn, scope, options, order) {
16715 var fnType = typeof fn,
16716 controller = args[args.length - 1],
16717 eventName = controller.getInfo().eventName,
16718 action;
16719
16720 args = Array.prototype.slice.call(args, 0, -2);
16721 args[0] = this;
16722
16723 if (fnType != 'undefined') {
16724 action = {
16725 fn: fn,
16726 scope: scope || this,
16727 options: options || {},
16728 order: order,
16729 isLateBinding: fnType == 'string'
16730 };
16731 }
16732
16733 return this.doFireEvent(eventName, args, action, controller);
16734 },
16735
16736 /**
16737 * @private
16738 * Creates an event handling function which re-fires the event from this object as the passed event name.
16739 * @param {String} newName
16740 * @return {Function}
16741 */
16742 createEventRelayer: function(newName){
16743 return function() {
16744 return this.doFireEvent(newName, Array.prototype.slice.call(arguments, 0, -2));
16745 }
16746 },
16747
16748 /**
16749 * Enables events fired by this Observable to bubble up an owner hierarchy by calling `this.getBubbleTarget()` if
16750 * present. There is no implementation in the Observable base class.
16751 *
16752 * @param {String/String[]} events The event name to bubble, or an Array of event names.
16753 */
16754 enableBubble: function(events) {
16755 var isBubblingEnabled = this.isBubblingEnabled,
16756 i, ln, name;
16757
16758 if (!isBubblingEnabled) {
16759 isBubblingEnabled = this.isBubblingEnabled = {};
16760 }
16761
16762 if (typeof events == 'string') {
16763 events = Ext.Array.clone(arguments);
16764 }
16765
16766 for (i = 0,ln = events.length; i < ln; i++) {
16767 name = events[i];
16768
16769 if (!isBubblingEnabled[name]) {
16770 isBubblingEnabled[name] = true;
16771 this.addListener(name, this.createEventBubbler(name), this);
16772 }
16773 }
16774 },
16775
16776 createEventBubbler: function(name) {
16777 return function doBubbleEvent() {
16778 var bubbleTarget = ('getBubbleTarget' in this) ? this.getBubbleTarget() : null;
16779
16780 if (bubbleTarget && bubbleTarget !== this && bubbleTarget.isObservable) {
16781 bubbleTarget.fireAction(name, Array.prototype.slice.call(arguments, 0, -2), doBubbleEvent, bubbleTarget, null, 'after');
16782 }
16783 }
16784 },
16785
16786 getBubbleTarget: function() {
16787 return false;
16788 },
16789
16790 destroy: function() {
16791 if (this.observableId) {
16792 this.fireEvent('destroy', this);
16793 this.clearListeners();
16794 this.clearManagedListeners();
16795 }
16796 },
16797
16798 /**
16799 * @ignore
16800 */
16801 addEvents: Ext.emptyFn
16802
16803 }, function() {
16804 this.createAlias({
16805 /**
16806 * @method
16807 * Alias for {@link #addListener}.
16808 * @inheritdoc Ext.mixin.Observable#addListener
16809 */
16810 on: 'addListener',
16811 /**
16812 * @method
16813 * Alias for {@link #removeListener}.
16814 * @inheritdoc Ext.mixin.Observable#removeListener
16815 */
16816 un: 'removeListener',
16817 /**
16818 * @method
16819 * Alias for {@link #addBeforeListener}.
16820 * @inheritdoc Ext.mixin.Observable#addBeforeListener
16821 */
16822 onBefore: 'addBeforeListener',
16823 /**
16824 * @method
16825 * Alias for {@link #addAfterListener}.
16826 * @inheritdoc Ext.mixin.Observable#addAfterListener
16827 */
16828 onAfter: 'addAfterListener',
16829 /**
16830 * @method
16831 * Alias for {@link #removeBeforeListener}.
16832 * @inheritdoc Ext.mixin.Observable#removeBeforeListener
16833 */
16834 unBefore: 'removeBeforeListener',
16835 /**
16836 * @method
16837 * Alias for {@link #removeAfterListener}.
16838 * @inheritdoc Ext.mixin.Observable#removeAfterListener
16839 */
16840 unAfter: 'removeAfterListener'
16841 });
16842
16843 });
16844
16845 /**
16846 * @private
16847 */
16848 Ext.define('Ext.Evented', {
16849
16850 alternateClassName: 'Ext.EventedBase',
16851
16852 mixins: [ Ext.mixin.Observable ],
16853
16854 statics: {
16855 generateSetter: function(nameMap) {
16856 var internalName = nameMap.internal,
16857 applyName = nameMap.apply,
16858 changeEventName = nameMap.changeEvent,
16859 doSetName = nameMap.doSet;
16860
16861 return function(value) {
16862 var initialized = this.initialized,
16863 oldValue = this[internalName],
16864 applier = this[applyName];
16865
16866 if (applier) {
16867 value = applier.call(this, value, oldValue);
16868
16869 if (typeof value == 'undefined') {
16870 return this;
16871 }
16872 }
16873
16874 // The old value might have been changed at this point
16875 // (after the apply call chain) so it should be read again
16876 oldValue = this[internalName];
16877
16878 if (value !== oldValue) {
16879 if (initialized) {
16880 this.fireAction(changeEventName, [this, value, oldValue], this.doSet, this, {
16881 nameMap: nameMap
16882 });
16883 }
16884 else {
16885 this[internalName] = value;
16886 if (this[doSetName]) {
16887 this[doSetName].call(this, value, oldValue);
16888 }
16889 }
16890 }
16891
16892 return this;
16893 }
16894 }
16895 },
16896
16897 initialized: false,
16898
16899 constructor: function(config) {
16900 this.initialConfig = config;
16901 this.initialize();
16902 },
16903
16904 initialize: function() {
16905 this.initConfig(this.initialConfig);
16906 this.initialized = true;
16907 },
16908
16909 doSet: function(me, value, oldValue, options) {
16910 var nameMap = options.nameMap;
16911
16912 me[nameMap.internal] = value;
16913 if (me[nameMap.doSet]) {
16914 me[nameMap.doSet].call(this, value, oldValue);
16915 }
16916 },
16917
16918 onClassExtended: function(Class, data) {
16919 if (!data.hasOwnProperty('eventedConfig')) {
16920 return;
16921 }
16922
16923 var ExtClass = Ext.Class,
16924 config = data.config,
16925 eventedConfig = data.eventedConfig,
16926 name, nameMap;
16927
16928 data.config = (config) ? Ext.applyIf(config, eventedConfig) : eventedConfig;
16929
16930 /*
16931 * These are generated setters for eventedConfig
16932 *
16933 * If the component is initialized, it invokes fireAction to fire the event as well,
16934 * which indicate something has changed. Otherwise, it just executes the action
16935 * (happens during initialization)
16936 *
16937 * This is helpful when we only want the event to be fired for subsequent changes.
16938 * Also it's a major performance improvement for instantiation when fired events
16939 * are mostly useless since there's no listeners
16940 */
16941 for (name in eventedConfig) {
16942 if (eventedConfig.hasOwnProperty(name)) {
16943 nameMap = ExtClass.getConfigNameMap(name);
16944
16945 data[nameMap.set] = this.generateSetter(nameMap);
16946 }
16947 }
16948 }
16949 });
16950
16951 /**
16952 * @private
16953 * This is the abstract class for {@link Ext.Component}.
16954 *
16955 * This should never be overridden.
16956 */
16957 Ext.define('Ext.AbstractComponent', {
16958 extend: Ext.Evented ,
16959
16960 onClassExtended: function(Class, members) {
16961 if (!members.hasOwnProperty('cachedConfig')) {
16962 return;
16963 }
16964
16965 var prototype = Class.prototype,
16966 config = members.config,
16967 cachedConfig = members.cachedConfig,
16968 cachedConfigList = prototype.cachedConfigList,
16969 hasCachedConfig = prototype.hasCachedConfig,
16970 name, value;
16971
16972 delete members.cachedConfig;
16973
16974 prototype.cachedConfigList = cachedConfigList = (cachedConfigList) ? cachedConfigList.slice() : [];
16975 prototype.hasCachedConfig = hasCachedConfig = (hasCachedConfig) ? Ext.Object.chain(hasCachedConfig) : {};
16976
16977 if (!config) {
16978 members.config = config = {};
16979 }
16980
16981 for (name in cachedConfig) {
16982 if (cachedConfig.hasOwnProperty(name)) {
16983 value = cachedConfig[name];
16984
16985 if (!hasCachedConfig[name]) {
16986 hasCachedConfig[name] = true;
16987 cachedConfigList.push(name);
16988 }
16989
16990 config[name] = value;
16991 }
16992 }
16993 },
16994
16995 getElementConfig: Ext.emptyFn,
16996
16997 referenceAttributeName: 'reference',
16998
16999 referenceSelector: '[reference]',
17000
17001 /**
17002 * @private
17003 * Significantly improve instantiation time for Component with multiple references
17004 * Ext.Element instance of the reference domNode is only created the very first time
17005 * it's ever used.
17006 */
17007 addReferenceNode: function(name, domNode) {
17008 Ext.Object.defineProperty(this, name, {
17009 get: function() {
17010 var reference;
17011
17012 delete this[name];
17013 this[name] = reference = new Ext.Element(domNode);
17014 return reference;
17015 },
17016 configurable: true
17017 });
17018 },
17019
17020 initElement: function() {
17021 var prototype = this.self.prototype,
17022 id = this.getId(),
17023 referenceList = [],
17024 cleanAttributes = true,
17025 referenceAttributeName = this.referenceAttributeName,
17026 needsOptimization = false,
17027 renderTemplate, renderElement, element,
17028 referenceNodes, i, ln, referenceNode, reference,
17029 configNameCache, defaultConfig, cachedConfigList, initConfigList, initConfigMap, configList,
17030 elements, name, nameMap, internalName;
17031
17032 if (prototype.hasOwnProperty('renderTemplate')) {
17033 renderTemplate = this.renderTemplate.cloneNode(true);
17034 renderElement = renderTemplate.firstChild;
17035 }
17036 else {
17037 cleanAttributes = false;
17038 needsOptimization = true;
17039 renderTemplate = document.createDocumentFragment();
17040 renderElement = Ext.Element.create(this.getElementConfig(), true);
17041 renderTemplate.appendChild(renderElement);
17042 }
17043
17044 referenceNodes = renderTemplate.querySelectorAll(this.referenceSelector);
17045
17046 for (i = 0,ln = referenceNodes.length; i < ln; i++) {
17047 referenceNode = referenceNodes[i];
17048 reference = referenceNode.getAttribute(referenceAttributeName);
17049
17050 if (cleanAttributes) {
17051 referenceNode.removeAttribute(referenceAttributeName);
17052 }
17053
17054 if (reference == 'element') {
17055 referenceNode.id = id;
17056 this.element = element = new Ext.Element(referenceNode);
17057 }
17058 else {
17059 this.addReferenceNode(reference, referenceNode);
17060 }
17061
17062 referenceList.push(reference);
17063 }
17064
17065 this.referenceList = referenceList;
17066
17067 if (!this.innerElement) {
17068 this.innerElement = element;
17069 }
17070
17071 if (!this.bodyElement) {
17072 this.bodyElement = this.innerElement;
17073 }
17074
17075 if (renderElement === element.dom) {
17076 this.renderElement = element;
17077 }
17078 else {
17079 this.addReferenceNode('renderElement', renderElement);
17080 }
17081
17082 // This happens only *once* per class, during the very first instantiation
17083 // to optimize renderTemplate based on cachedConfig
17084 if (needsOptimization) {
17085 configNameCache = Ext.Class.configNameCache;
17086 defaultConfig = this.config;
17087 cachedConfigList = this.cachedConfigList;
17088 initConfigList = this.initConfigList;
17089 initConfigMap = this.initConfigMap;
17090 configList = [];
17091
17092 for (i = 0,ln = cachedConfigList.length; i < ln; i++) {
17093 name = cachedConfigList[i];
17094 nameMap = configNameCache[name];
17095
17096 if (initConfigMap[name]) {
17097 initConfigMap[name] = false;
17098 Ext.Array.remove(initConfigList, name);
17099 }
17100
17101 if (defaultConfig[name] !== null) {
17102 configList.push(name);
17103 this[nameMap.get] = this[nameMap.initGet];
17104 }
17105 }
17106
17107 for (i = 0,ln = configList.length; i < ln; i++) {
17108 name = configList[i];
17109 nameMap = configNameCache[name];
17110 internalName = nameMap.internal;
17111
17112 this[internalName] = null;
17113 this[nameMap.set].call(this, defaultConfig[name]);
17114 delete this[nameMap.get];
17115
17116 prototype[internalName] = this[internalName];
17117 }
17118
17119 renderElement = this.renderElement.dom;
17120 prototype.renderTemplate = renderTemplate = document.createDocumentFragment();
17121 renderTemplate.appendChild(renderElement.cloneNode(true));
17122
17123 elements = renderTemplate.querySelectorAll('[id]');
17124
17125 for (i = 0,ln = elements.length; i < ln; i++) {
17126 element = elements[i];
17127 element.removeAttribute('id');
17128 }
17129
17130 for (i = 0,ln = referenceList.length; i < ln; i++) {
17131 reference = referenceList[i];
17132 this[reference].dom.removeAttribute('reference');
17133 }
17134 }
17135
17136 return this;
17137 }
17138 });
17139
17140 /**
17141 * Represents a collection of a set of key and value pairs. Each key in the HashMap must be unique, the same
17142 * key cannot exist twice. Access to items is provided via the key only. Sample usage:
17143 *
17144 * var map = Ext.create('Ext.util.HashMap');
17145 * map.add('key1', 1);
17146 * map.add('key2', 2);
17147 * map.add('key3', 3);
17148 *
17149 * map.each(function(key, value, length){
17150 * console.log(key, value, length);
17151 * });
17152 *
17153 * The HashMap is an unordered class, there is no guarantee when iterating over the items that they will be in
17154 * any particular order. If this is required, then use a {@link Ext.util.MixedCollection}.
17155 */
17156 Ext.define('Ext.util.HashMap', {
17157 mixins: {
17158 observable: Ext.mixin.Observable
17159 },
17160
17161 /**
17162 * @cfg {Function} keyFn
17163 * A function that is used to retrieve a default key for a passed object.
17164 * A default is provided that returns the **id** property on the object.
17165 * This function is only used if the add method is called with a single argument.
17166 */
17167
17168 /**
17169 * Creates new HashMap.
17170 * @param {Object} config The configuration options
17171 */
17172 constructor: function(config) {
17173 /**
17174 * @event add
17175 * Fires when a new item is added to the hash.
17176 * @param {Ext.util.HashMap} this
17177 * @param {String} key The key of the added item.
17178 * @param {Object} value The value of the added item.
17179 */
17180 /**
17181 * @event clear
17182 * Fires when the hash is cleared.
17183 * @param {Ext.util.HashMap} this
17184 */
17185 /**
17186 * @event remove
17187 * Fires when an item is removed from the hash.
17188 * @param {Ext.util.HashMap} this
17189 * @param {String} key The key of the removed item.
17190 * @param {Object} value The value of the removed item.
17191 */
17192 /**
17193 * @event replace
17194 * Fires when an item is replaced in the hash.
17195 * @param {Ext.util.HashMap} this
17196 * @param {String} key The key of the replaced item.
17197 * @param {Object} value The new value for the item.
17198 * @param {Object} old The old value for the item.
17199 */
17200
17201 this.callParent();
17202
17203 this.mixins.observable.constructor.call(this);
17204
17205 this.clear(true);
17206 },
17207
17208 /**
17209 * Gets the number of items in the hash.
17210 * @return {Number} The number of items in the hash.
17211 */
17212 getCount: function() {
17213 return this.length;
17214 },
17215
17216 /**
17217 * Implementation for being able to extract the key from an object if only
17218 * a single argument is passed.
17219 * @private
17220 * @param {String} key The key
17221 * @param {Object} value The value
17222 * @return {Array} [key, value]
17223 */
17224 getData: function(key, value) {
17225 // if we have no value, it means we need to get the key from the object
17226 if (value === undefined) {
17227 value = key;
17228 key = this.getKey(value);
17229 }
17230
17231 return [key, value];
17232 },
17233
17234 /**
17235 * Extracts the key from an object. This is a default implementation, it may be overridden.
17236 * @private
17237 * @param {Object} o The object to get the key from.
17238 * @return {String} The key to use.
17239 */
17240 getKey: function(o) {
17241 return o.id;
17242 },
17243
17244 /**
17245 * Add a new item to the hash. An exception will be thrown if the key already exists.
17246 * @param {String} key The key of the new item.
17247 * @param {Object} value The value of the new item.
17248 * @return {Object} The value of the new item added.
17249 */
17250 add: function(key, value) {
17251 var me = this,
17252 data;
17253
17254 if (me.containsKey(key)) {
17255 throw new Error('This key already exists in the HashMap');
17256 }
17257
17258 data = this.getData(key, value);
17259 key = data[0];
17260 value = data[1];
17261 me.map[key] = value;
17262 ++me.length;
17263 me.fireEvent('add', me, key, value);
17264 return value;
17265 },
17266
17267 /**
17268 * Replaces an item in the hash. If the key doesn't exist, the
17269 * `{@link #method-add}` method will be used.
17270 * @param {String} key The key of the item.
17271 * @param {Object} value The new value for the item.
17272 * @return {Object} The new value of the item.
17273 */
17274 replace: function(key, value) {
17275 var me = this,
17276 map = me.map,
17277 old;
17278
17279 if (!me.containsKey(key)) {
17280 me.add(key, value);
17281 }
17282 old = map[key];
17283 map[key] = value;
17284 me.fireEvent('replace', me, key, value, old);
17285 return value;
17286 },
17287
17288 /**
17289 * Remove an item from the hash.
17290 * @param {Object} o The value of the item to remove.
17291 * @return {Boolean} `true` if the item was successfully removed.
17292 */
17293 remove: function(o) {
17294 var key = this.findKey(o);
17295 if (key !== undefined) {
17296 return this.removeByKey(key);
17297 }
17298 return false;
17299 },
17300
17301 /**
17302 * Remove an item from the hash.
17303 * @param {String} key The key to remove.
17304 * @return {Boolean} `true` if the item was successfully removed.
17305 */
17306 removeByKey: function(key) {
17307 var me = this,
17308 value;
17309
17310 if (me.containsKey(key)) {
17311 value = me.map[key];
17312 delete me.map[key];
17313 --me.length;
17314 me.fireEvent('remove', me, key, value);
17315 return true;
17316 }
17317 return false;
17318 },
17319
17320 /**
17321 * Retrieves an item with a particular key.
17322 * @param {String} key The key to lookup.
17323 * @return {Object} The value at that key. If it doesn't exist, `undefined` is returned.
17324 */
17325 get: function(key) {
17326 return this.map[key];
17327 },
17328
17329 /**
17330 * Removes all items from the hash.
17331 * @return {Ext.util.HashMap} this
17332 */
17333 clear: function(/* private */ initial) {
17334 var me = this;
17335 me.map = {};
17336 me.length = 0;
17337 if (initial !== true) {
17338 me.fireEvent('clear', me);
17339 }
17340 return me;
17341 },
17342
17343 /**
17344 * Checks whether a key exists in the hash.
17345 * @param {String} key The key to check for.
17346 * @return {Boolean} `true` if they key exists in the hash.
17347 */
17348 containsKey: function(key) {
17349 return this.map[key] !== undefined;
17350 },
17351
17352 /**
17353 * Checks whether a value exists in the hash.
17354 * @param {Object} value The value to check for.
17355 * @return {Boolean} `true` if the value exists in the dictionary.
17356 */
17357 contains: function(value) {
17358 return this.containsKey(this.findKey(value));
17359 },
17360
17361 /**
17362 * Return all of the keys in the hash.
17363 * @return {Array} An array of keys.
17364 */
17365 getKeys: function() {
17366 return this.getArray(true);
17367 },
17368
17369 /**
17370 * Return all of the values in the hash.
17371 * @return {Array} An array of values.
17372 */
17373 getValues: function() {
17374 return this.getArray(false);
17375 },
17376
17377 /**
17378 * Gets either the keys/values in an array from the hash.
17379 * @private
17380 * @param {Boolean} isKey `true` to extract the keys, otherwise, the value.
17381 * @return {Array} An array of either keys/values from the hash.
17382 */
17383 getArray: function(isKey) {
17384 var arr = [],
17385 key,
17386 map = this.map;
17387 for (key in map) {
17388 if (map.hasOwnProperty(key)) {
17389 arr.push(isKey ? key : map[key]);
17390 }
17391 }
17392 return arr;
17393 },
17394
17395 /**
17396 * Executes the specified function once for each item in the hash.
17397 *
17398 * @param {Function} fn The function to execute.
17399 * @param {String} fn.key The key of the item.
17400 * @param {Number} fn.value The value of the item.
17401 * @param {Number} fn.length The total number of items in the hash.
17402 * @param {Boolean} fn.return Returning `false` from the function will cease the iteration.
17403 * @param {Object} [scope=this] The scope to execute in.
17404 * @return {Ext.util.HashMap} this
17405 */
17406 each: function(fn, scope) {
17407 // copy items so they may be removed during iteration.
17408 var items = Ext.apply({}, this.map),
17409 key,
17410 length = this.length;
17411
17412 scope = scope || this;
17413 for (key in items) {
17414 if (items.hasOwnProperty(key)) {
17415 if (fn.call(scope, key, items[key], length) === false) {
17416 break;
17417 }
17418 }
17419 }
17420 return this;
17421 },
17422
17423 /**
17424 * Performs a shallow copy on this hash.
17425 * @return {Ext.util.HashMap} The new hash object.
17426 */
17427 clone: function() {
17428 var hash = new Ext.util.HashMap(),
17429 map = this.map,
17430 key;
17431
17432 hash.suspendEvents();
17433 for (key in map) {
17434 if (map.hasOwnProperty(key)) {
17435 hash.add(key, map[key]);
17436 }
17437 }
17438 hash.resumeEvents();
17439 return hash;
17440 },
17441
17442 /**
17443 * @private
17444 * Find the key for a value.
17445 * @param {Object} value The value to find.
17446 * @return {Object} The value of the item. Returns `undefined` if not found.
17447 */
17448 findKey: function(value) {
17449 var key,
17450 map = this.map;
17451
17452 for (key in map) {
17453 if (map.hasOwnProperty(key) && map[key] === value) {
17454 return key;
17455 }
17456 }
17457 return undefined;
17458 }
17459 });
17460
17461 /**
17462 * @private
17463 */
17464 Ext.define('Ext.AbstractManager', {
17465
17466 /* Begin Definitions */
17467
17468
17469
17470 /* End Definitions */
17471
17472 typeName: 'type',
17473
17474 constructor: function(config) {
17475 Ext.apply(this, config || {});
17476
17477 /**
17478 * @property {Ext.util.HashMap} all
17479 * Contains all of the items currently managed
17480 */
17481 this.all = Ext.create('Ext.util.HashMap');
17482
17483 this.types = {};
17484 },
17485
17486 /**
17487 * Returns an item by id.
17488 * For additional details see {@link Ext.util.HashMap#get}.
17489 * @param {String} id The `id` of the item.
17490 * @return {Object} The item, `undefined` if not found.
17491 */
17492 get : function(id) {
17493 return this.all.get(id);
17494 },
17495
17496 /**
17497 * Registers an item to be managed.
17498 * @param {Object} item The item to register.
17499 */
17500 register: function(item) {
17501 this.all.add(item);
17502 },
17503
17504 /**
17505 * Unregisters an item by removing it from this manager.
17506 * @param {Object} item The item to unregister.
17507 */
17508 unregister: function(item) {
17509 this.all.remove(item);
17510 },
17511
17512 /**
17513 * Registers a new item constructor, keyed by a type key.
17514 * @param {String} type The mnemonic string by which the class may be looked up.
17515 * @param {Function} cls The new instance class.
17516 */
17517 registerType : function(type, cls) {
17518 this.types[type] = cls;
17519 cls[this.typeName] = type;
17520 },
17521
17522 /**
17523 * Checks if an item type is registered.
17524 * @param {String} type The mnemonic string by which the class may be looked up.
17525 * @return {Boolean} Whether the type is registered.
17526 */
17527 isRegistered : function(type){
17528 return this.types[type] !== undefined;
17529 },
17530
17531 /**
17532 * Creates and returns an instance of whatever this manager manages, based on the supplied type and
17533 * config object.
17534 * @param {Object} config The config object.
17535 * @param {String} defaultType If no type is discovered in the config object, we fall back to this type.
17536 * @return {Object} The instance of whatever this manager is managing.
17537 */
17538 create: function(config, defaultType) {
17539 var type = config[this.typeName] || config.type || defaultType,
17540 Constructor = this.types[type];
17541
17542 //<debug>
17543 if (Constructor == undefined) {
17544 Ext.Error.raise("The '" + type + "' type has not been registered with this manager");
17545 }
17546 //</debug>
17547
17548 return new Constructor(config);
17549 },
17550
17551 /**
17552 * Registers a function that will be called when an item with the specified id is added to the manager.
17553 * This will happen on instantiation.
17554 * @param {String} id The item `id`.
17555 * @param {Function} fn The callback function. Called with a single parameter, the item.
17556 * @param {Object} scope The scope (`this` reference) in which the callback is executed.
17557 * Defaults to the item.
17558 */
17559 onAvailable : function(id, fn, scope){
17560 var all = this.all,
17561 item;
17562
17563 if (all.containsKey(id)) {
17564 item = all.get(id);
17565 fn.call(scope || item, item);
17566 } else {
17567 all.on('add', function(map, key, item){
17568 if (key == id) {
17569 fn.call(scope || item, item);
17570 all.un('add', fn, scope);
17571 }
17572 });
17573 }
17574 },
17575
17576 /**
17577 * Executes the specified function once for each item in the collection.
17578 * @param {Function} fn The function to execute.
17579 * @param {String} fn.key The key of the item
17580 * @param {Number} fn.value The value of the item
17581 * @param {Number} fn.length The total number of items in the collection
17582 * @param {Boolean} fn.return False to cease iteration.
17583 * @param {Object} [scope=this] The scope to execute in.
17584 */
17585 each: function(fn, scope){
17586 this.all.each(fn, scope || this);
17587 },
17588
17589 /**
17590 * Gets the number of items in the collection.
17591 * @return {Number} The number of items in the collection.
17592 */
17593 getCount: function(){
17594 return this.all.getCount();
17595 }
17596 });
17597
17598 /**
17599 * @private
17600 * This is a compatibility class.
17601 */
17602 Ext.define('Ext.AbstractPlugin', {});
17603
17604 /**
17605 * A Traversable mixin.
17606 * @private
17607 */
17608 Ext.define('Ext.mixin.Traversable', {
17609 extend: Ext.mixin.Mixin ,
17610
17611 mixinConfig: {
17612 id: 'traversable'
17613 },
17614
17615 setParent: function(parent) {
17616 this.parent = parent;
17617
17618 return this;
17619 },
17620
17621 /**
17622 * @member Ext.Component
17623 * Returns `true` if this component has a parent.
17624 * @return {Boolean} `true` if this component has a parent.
17625 */
17626 hasParent: function() {
17627 return Boolean(this.parent);
17628 },
17629
17630 /**
17631 * @member Ext.Component
17632 * Returns the parent of this component, if it has one.
17633 * @return {Ext.Component} The parent of this component.
17634 */
17635 getParent: function() {
17636 return this.parent;
17637 },
17638
17639 getAncestors: function() {
17640 var ancestors = [],
17641 parent = this.getParent();
17642
17643 while (parent) {
17644 ancestors.push(parent);
17645 parent = parent.getParent();
17646 }
17647
17648 return ancestors;
17649 },
17650
17651 getAncestorIds: function() {
17652 var ancestorIds = [],
17653 parent = this.getParent();
17654
17655 while (parent) {
17656 ancestorIds.push(parent.getId());
17657 parent = parent.getParent();
17658 }
17659
17660 return ancestorIds;
17661 }
17662 });
17663
17664 /**
17665 * @private
17666 *
17667 * Provides a registry of all Components (instances of {@link Ext.Component} or any subclass
17668 * thereof) on a page so that they can be easily accessed by {@link Ext.Component component}
17669 * {@link Ext.Component#getId id} (see {@link #get}, or the convenience method {@link Ext#getCmp Ext.getCmp}).
17670 *
17671 * This object also provides a registry of available Component _classes_
17672 * indexed by a mnemonic code known as the Component's `xtype`.
17673 * The `xtype` provides a way to avoid instantiating child Components
17674 * when creating a full, nested config object for a complete Ext page.
17675 *
17676 * A child Component may be specified simply as a _config object_
17677 * as long as the correct `xtype` is specified so that if and when the Component
17678 * needs rendering, the correct type can be looked up for lazy instantiation.
17679 *
17680 * For a list of all available `xtype`, see {@link Ext.Component}.
17681 */
17682 Ext.define('Ext.ComponentManager', {
17683 alternateClassName: 'Ext.ComponentMgr',
17684 singleton: true,
17685
17686 constructor: function() {
17687 var map = {};
17688
17689 // The sole reason for this is just to support the old code of ComponentQuery
17690 this.all = {
17691 map: map,
17692
17693 getArray: function() {
17694 var list = [],
17695 id;
17696
17697 for (id in map) {
17698 if (map.hasOwnProperty(id)) {
17699 list.push(map[id]);
17700 }
17701 }
17702 return list;
17703 }
17704 };
17705
17706 this.map = map;
17707 },
17708
17709 /**
17710 * Registers an item to be managed.
17711 * @param {Object} component The item to register.
17712 */
17713 register: function(component) {
17714 var id = component.getId();
17715
17716 // <debug>
17717 if (this.map[id]) {
17718 Ext.Logger.warn('Registering a component with a id (`' + id + '`) which has already been used. Please ensure the existing component has been destroyed (`Ext.Component#destroy()`.');
17719 }
17720 // </debug>
17721
17722 this.map[component.getId()] = component;
17723 },
17724
17725 /**
17726 * Unregisters an item by removing it from this manager.
17727 * @param {Object} component The item to unregister.
17728 */
17729 unregister: function(component) {
17730 delete this.map[component.getId()];
17731 },
17732
17733 /**
17734 * Checks if an item type is registered.
17735 * @param {String} component The mnemonic string by which the class may be looked up.
17736 * @return {Boolean} Whether the type is registered.
17737 */
17738 isRegistered : function(component){
17739 return this.map[component] !== undefined;
17740 },
17741
17742 /**
17743 * Returns an item by id.
17744 * For additional details see {@link Ext.util.HashMap#get}.
17745 * @param {String} id The `id` of the item.
17746 * @return {Object} The item, or `undefined` if not found.
17747 */
17748 get: function(id) {
17749 return this.map[id];
17750 },
17751
17752 /**
17753 * Creates a new Component from the specified config object using the
17754 * config object's `xtype` to determine the class to instantiate.
17755 * @param {Object} component A configuration object for the Component you wish to create.
17756 * @param {Function} [defaultType] The constructor to provide the default Component type if
17757 * the config object does not contain a `xtype`. (Optional if the config contains an `xtype`).
17758 * @return {Ext.Component} The newly instantiated Component.
17759 */
17760 create: function(component, defaultType) {
17761 if (component.isComponent) {
17762 return component;
17763 }
17764 else if (Ext.isString(component)) {
17765 return Ext.createByAlias('widget.' + component);
17766 }
17767 else {
17768 var type = component.xtype || defaultType;
17769
17770 return Ext.createByAlias('widget.' + type, component);
17771 }
17772 },
17773
17774 registerType: Ext.emptyFn
17775 });
17776
17777 //@define Ext.DateExtras
17778 /**
17779 * @class Ext.Date
17780 * @mixins Ext.DateExtras
17781 * A set of useful static methods to deal with date.
17782 *
17783 * __Note:__ Unless you require `Ext.DateExtras`, only the {@link #now} method will be available. You **MUST**
17784 * require `Ext.DateExtras` to use the other methods available below.
17785 *
17786 * Usage with {@link Ext#setup}:
17787 *
17788 * @example
17789 * Ext.setup({
17790 * requires: 'Ext.DateExtras',
17791 * onReady: function() {
17792 * var date = new Date();
17793 * alert(Ext.Date.format(date, 'n/j/Y'));
17794 * }
17795 * });
17796 *
17797 * The date parsing and formatting syntax contains a subset of
17798 * [PHP's `date()` function](http://www.php.net/date), and the formats that are
17799 * supported will provide results equivalent to their PHP versions.
17800 *
17801 * The following is a list of all currently supported formats:
17802 * <pre>
17803 Format Description Example returned values
17804 ------ ----------------------------------------------------------------------- -----------------------
17805 d Day of the month, 2 digits with leading zeros 01 to 31
17806 D A short textual representation of the day of the week Mon to Sun
17807 j Day of the month without leading zeros 1 to 31
17808 l A full textual representation of the day of the week Sunday to Saturday
17809 N ISO-8601 numeric representation of the day of the week 1 (for Monday) through 7 (for Sunday)
17810 S English ordinal suffix for the day of the month, 2 characters st, nd, rd or th. Works well with j
17811 w Numeric representation of the day of the week 0 (for Sunday) to 6 (for Saturday)
17812 z The day of the year (starting from 0) 0 to 364 (365 in leap years)
17813 W ISO-8601 week number of year, weeks starting on Monday 01 to 53
17814 F A full textual representation of a month, such as January or March January to December
17815 m Numeric representation of a month, with leading zeros 01 to 12
17816 M A short textual representation of a month Jan to Dec
17817 n Numeric representation of a month, without leading zeros 1 to 12
17818 t Number of days in the given month 28 to 31
17819 L Whether it&#39;s a leap year 1 if it is a leap year, 0 otherwise.
17820 o ISO-8601 year number (identical to (Y), but if the ISO week number (W) Examples: 1998 or 2004
17821 belongs to the previous or next year, that year is used instead)
17822 Y A full numeric representation of a year, 4 digits Examples: 1999 or 2003
17823 y A two digit representation of a year Examples: 99 or 03
17824 a Lowercase Ante meridiem and Post meridiem am or pm
17825 A Uppercase Ante meridiem and Post meridiem AM or PM
17826 g 12-hour format of an hour without leading zeros 1 to 12
17827 G 24-hour format of an hour without leading zeros 0 to 23
17828 h 12-hour format of an hour with leading zeros 01 to 12
17829 H 24-hour format of an hour with leading zeros 00 to 23
17830 i Minutes, with leading zeros 00 to 59
17831 s Seconds, with leading zeros 00 to 59
17832 u Decimal fraction of a second Examples:
17833 (minimum 1 digit, arbitrary number of digits allowed) 001 (i.e. 0.001s) or
17834 100 (i.e. 0.100s) or
17835 999 (i.e. 0.999s) or
17836 999876543210 (i.e. 0.999876543210s)
17837 O Difference to Greenwich time (GMT) in hours and minutes Example: +1030
17838 P Difference to Greenwich time (GMT) with colon between hours and minutes Example: -08:00
17839 T Timezone abbreviation of the machine running the code Examples: EST, MDT, PDT ...
17840 Z Timezone offset in seconds (negative if west of UTC, positive if east) -43200 to 50400
17841 c ISO 8601 date
17842 Notes: Examples:
17843 1) If unspecified, the month / day defaults to the current month / day, 1991 or
17844 the time defaults to midnight, while the timezone defaults to the 1992-10 or
17845 browser's timezone. If a time is specified, it must include both hours 1993-09-20 or
17846 and minutes. The "T" delimiter, seconds, milliseconds and timezone 1994-08-19T16:20+01:00 or
17847 are optional. 1995-07-18T17:21:28-02:00 or
17848 2) The decimal fraction of a second, if specified, must contain at 1996-06-17T18:22:29.98765+03:00 or
17849 least 1 digit (there is no limit to the maximum number 1997-05-16T19:23:30,12345-0400 or
17850 of digits allowed), and may be delimited by either a '.' or a ',' 1998-04-15T20:24:31.2468Z or
17851 Refer to the examples on the right for the various levels of 1999-03-14T20:24:32Z or
17852 date-time granularity which are supported, or see 2000-02-13T21:25:33
17853 http://www.w3.org/TR/NOTE-datetime for more info. 2001-01-12 22:26:34
17854 U Seconds since the Unix Epoch (January 1 1970 00:00:00 GMT) 1193432466 or -2138434463
17855 MS Microsoft AJAX serialized dates \/Date(1238606590509)\/ (i.e. UTC milliseconds since epoch) or
17856 \/Date(1238606590509+0800)\/
17857 </pre>
17858 *
17859 * For more information on the ISO 8601 date/time format, see [http://www.w3.org/TR/NOTE-datetime](http://www.w3.org/TR/NOTE-datetime).
17860 *
17861 * Example usage (note that you must escape format specifiers with '\\' to render them as character literals):
17862 *
17863 * // Sample date:
17864 * // 'Wed Jan 10 2007 15:05:01 GMT-0600 (Central Standard Time)'
17865 *
17866 * var dt = new Date('1/10/2007 03:05:01 PM GMT-0600');
17867 * console.log(Ext.Date.format(dt, 'Y-m-d')); // 2007-01-10
17868 * console.log(Ext.Date.format(dt, 'F j, Y, g:i a')); // January 10, 2007, 3:05 pm
17869 * console.log(Ext.Date.format(dt, 'l, \\t\\he jS \\of F Y h:i:s A')); // Wednesday, the 10th of January 2007 03:05:01 PM
17870 *
17871 * Here are some standard date/time patterns that you might find helpful. They
17872 * are not part of the source of Ext.Date, but to use them you can simply copy this
17873 * block of code into any script that is included after Ext.Date and they will also become
17874 * globally available on the Date object. Feel free to add or remove patterns as needed in your code.
17875 *
17876 * Ext.Date.patterns = {
17877 * ISO8601Long: "Y-m-d H:i:s",
17878 * ISO8601Short: "Y-m-d",
17879 * ShortDate: "n/j/Y",
17880 * LongDate: "l, F d, Y",
17881 * FullDateTime: "l, F d, Y g:i:s A",
17882 * MonthDay: "F d",
17883 * ShortTime: "g:i A",
17884 * LongTime: "g:i:s A",
17885 * SortableDateTime: "Y-m-d\\TH:i:s",
17886 * UniversalSortableDateTime: "Y-m-d H:i:sO",
17887 * YearMonth: "F, Y"
17888 * };
17889 *
17890 * Example usage:
17891 *
17892 * @example
17893 * var dt = new Date();
17894 * Ext.Date.patterns = {
17895 * ShortDate: "n/j/Y"
17896 * };
17897 * alert(Ext.Date.format(dt, Ext.Date.patterns.ShortDate));
17898 *
17899 * Developer-written, custom formats may be used by supplying both a formatting and a parsing function
17900 * which perform to specialized requirements. The functions are stored in {@link #parseFunctions} and {@link #formatFunctions}.
17901 * @singleton
17902 */
17903
17904 /*
17905 * Most of the date-formatting functions below are the excellent work of Baron Schwartz.
17906 * see http://www.xaprb.com/blog/2005/12/12/javascript-closures-for-runtime-efficiency/)
17907 * They generate precompiled functions from format patterns instead of parsing and
17908 * processing each pattern every time a date is formatted. These functions are available
17909 * on every Date object.
17910 */
17911
17912 (function() {
17913
17914 // create private copy of Ext's Ext.util.Format.format() method
17915 // - to remove unnecessary dependency
17916 // - to resolve namespace conflict with MS-Ajax's implementation
17917 function xf(format) {
17918 var args = Array.prototype.slice.call(arguments, 1);
17919 return format.replace(/\{(\d+)\}/g, function(m, i) {
17920 return args[i];
17921 });
17922 }
17923
17924 /**
17925 * Extra methods to be mixed into Ext.Date.
17926 *
17927 * Require this class to get Ext.Date with all the methods listed below.
17928 *
17929 * Using Ext.setup:
17930 *
17931 * @example
17932 * Ext.setup({
17933 * requires: 'Ext.DateExtras',
17934 * onReady: function() {
17935 * var date = new Date();
17936 * alert(Ext.Date.format(date, 'n/j/Y'));
17937 * }
17938 * });
17939 *
17940 * Using Ext.application:
17941 *
17942 * @example
17943 * Ext.application({
17944 * requires: 'Ext.DateExtras',
17945 * launch: function() {
17946 * var date = new Date();
17947 * alert(Ext.Date.format(date, 'n/j/Y'));
17948 * }
17949 * });
17950 *
17951 * @singleton
17952 */
17953 Ext.DateExtras = {
17954 /**
17955 * Returns the current timestamp.
17956 * @return {Number} The current timestamp.
17957 * @method
17958 */
17959 now: Date.now || function() {
17960 return +new Date();
17961 },
17962
17963 /**
17964 * Returns the number of milliseconds between two dates.
17965 * @param {Date} dateA The first date.
17966 * @param {Date} [dateB=new Date()] (optional) The second date, defaults to now.
17967 * @return {Number} The difference in milliseconds.
17968 */
17969 getElapsed: function(dateA, dateB) {
17970 return Math.abs(dateA - (dateB || new Date()));
17971 },
17972
17973 /**
17974 * Global flag which determines if strict date parsing should be used.
17975 * Strict date parsing will not roll-over invalid dates, which is the
17976 * default behavior of JavaScript Date objects.
17977 * (see {@link #parse} for more information)
17978 * @type Boolean
17979 */
17980 useStrict: false,
17981
17982 // @private
17983 formatCodeToRegex: function(character, currentGroup) {
17984 // Note: currentGroup - position in regex result array (see notes for Ext.Date.parseCodes below)
17985 var p = utilDate.parseCodes[character];
17986
17987 if (p) {
17988 p = typeof p == 'function'? p() : p;
17989 utilDate.parseCodes[character] = p; // reassign function result to prevent repeated execution
17990 }
17991
17992 return p ? Ext.applyIf({
17993 c: p.c ? xf(p.c, currentGroup || "{0}") : p.c
17994 }, p) : {
17995 g: 0,
17996 c: null,
17997 s: Ext.String.escapeRegex(character) // treat unrecognized characters as literals
17998 };
17999 },
18000
18001 /**
18002 * An object hash in which each property is a date parsing function. The property name is the
18003 * format string which that function parses.
18004 *
18005 * This object is automatically populated with date parsing functions as
18006 * date formats are requested for Ext standard formatting strings.
18007 *
18008 * Custom parsing functions may be inserted into this object, keyed by a name which from then on
18009 * may be used as a format string to {@link #parse}.
18010 *
18011 * Example:
18012 *
18013 * Ext.Date.parseFunctions['x-date-format'] = myDateParser;
18014 *
18015 * A parsing function should return a Date object, and is passed the following parameters:
18016 *
18017 * - `date`: {@link String} - The date string to parse.
18018 * - `strict`: {@link Boolean} - `true` to validate date strings while parsing
18019 * (i.e. prevent JavaScript Date "rollover"). __The default must be `false`.__
18020 * Invalid date strings should return `null` when parsed.
18021 *
18022 * To enable Dates to also be _formatted_ according to that format, a corresponding
18023 * formatting function must be placed into the {@link #formatFunctions} property.
18024 * @property parseFunctions
18025 * @type Object
18026 */
18027 parseFunctions: {
18028 "MS": function(input, strict) {
18029 // note: the timezone offset is ignored since the MS Ajax server sends
18030 // a UTC milliseconds-since-Unix-epoch value (negative values are allowed)
18031 var re = new RegExp('\\\\?/Date\\(([-+])?(\\d+)(?:[+-]\\d{4})?\\)\\\\?/');
18032 var r = (input || '').match(re);
18033 return r? new Date(((r[1] || '') + r[2]) * 1) : null;
18034 }
18035 },
18036 parseRegexes: [],
18037
18038 /**
18039 * An object hash in which each property is a date formatting function. The property name is the
18040 * format string which corresponds to the produced formatted date string.
18041 *
18042 * This object is automatically populated with date formatting functions as
18043 * date formats are requested for Ext standard formatting strings.
18044 *
18045 * Custom formatting functions may be inserted into this object, keyed by a name which from then on
18046 * may be used as a format string to {@link #format}.
18047 *
18048 * Example:
18049 *
18050 * Ext.Date.formatFunctions['x-date-format'] = myDateFormatter;
18051 *
18052 * A formatting function should return a string representation of the Date object which is the scope (this) of the function.
18053 *
18054 * To enable date strings to also be _parsed_ according to that format, a corresponding
18055 * parsing function must be placed into the {@link #parseFunctions} property.
18056 * @property formatFunctions
18057 * @type Object
18058 */
18059 formatFunctions: {
18060 "MS": function() {
18061 // UTC milliseconds since Unix epoch (MS-AJAX serialized date format (MRSF))
18062 return '\\/Date(' + this.getTime() + ')\\/';
18063 }
18064 },
18065
18066 y2kYear : 50,
18067
18068 /**
18069 * Date interval constant.
18070 * @type String
18071 * @readonly
18072 */
18073 MILLI : "ms",
18074
18075 /**
18076 * Date interval constant.
18077 * @type String
18078 * @readonly
18079 */
18080 SECOND : "s",
18081
18082 /**
18083 * Date interval constant.
18084 * @type String
18085 * @readonly
18086 */
18087 MINUTE : "mi",
18088
18089 /**
18090 * Date interval constant.
18091 * @type String
18092 * @readonly
18093 */
18094 HOUR : "h",
18095
18096 /**
18097 * Date interval constant.
18098 * @type String
18099 * @readonly
18100 */
18101 DAY : "d",
18102
18103 /**
18104 * Date interval constant.
18105 * @type String
18106 * @readonly
18107 */
18108 MONTH : "mo",
18109
18110 /**
18111 * Date interval constant.
18112 * @type String
18113 * @readonly
18114 */
18115 YEAR : "y",
18116
18117 /**
18118 * An object hash containing default date values used during date parsing.
18119 *
18120 * The following properties are available:
18121 *
18122 * - `y`: {@link Number} - The default year value. Defaults to `undefined`.
18123 * - `m`: {@link Number} - The default 1-based month value. Defaults to `undefined`.
18124 * - `d`: {@link Number} - The default day value. Defaults to `undefined`.
18125 * - `h`: {@link Number} - The default hour value. Defaults to `undefined`.
18126 * - `i`: {@link Number} - The default minute value. Defaults to `undefined`.
18127 * - `s`: {@link Number} - The default second value. Defaults to `undefined`.
18128 * - `ms`: {@link Number} - The default millisecond value. Defaults to `undefined`.
18129 *
18130 * Override these properties to customize the default date values used by the {@link #parse} method.
18131 *
18132 * __Note:__ In countries which experience Daylight Saving Time (i.e. DST), the `h`, `i`, `s`
18133 * and `ms` properties may coincide with the exact time in which DST takes effect.
18134 * It is the responsibility of the developer to account for this.
18135 *
18136 * Example Usage:
18137 *
18138 * @example
18139 * // set default day value to the first day of the month
18140 * Ext.Date.defaults.d = 1;
18141 *
18142 * // parse a February date string containing only year and month values.
18143 * // setting the default day value to 1 prevents weird date rollover issues.
18144 * // when attempting to parse the following date string on, for example, March 31st 2009.
18145 * alert(Ext.Date.parse('2009-02', 'Y-m')); // returns a Date object representing February 1st 2009.
18146 *
18147 * @property defaults
18148 * @type Object
18149 */
18150 defaults: {},
18151
18152 /**
18153 * An array of textual day names.
18154 * Override these values for international dates.
18155 * Example:
18156 *
18157 * Ext.Date.dayNames = [
18158 * 'SundayInYourLang',
18159 * 'MondayInYourLang'
18160 * // ...
18161 * ];
18162 *
18163 * @type Array
18164 */
18165 dayNames : [
18166 "Sunday",
18167 "Monday",
18168 "Tuesday",
18169 "Wednesday",
18170 "Thursday",
18171 "Friday",
18172 "Saturday"
18173 ],
18174
18175 /**
18176 * An array of textual month names.
18177 * Override these values for international dates.
18178 * Example:
18179 *
18180 * Ext.Date.monthNames = [
18181 * 'JanInYourLang',
18182 * 'FebInYourLang'
18183 * // ...
18184 * ];
18185 *
18186 * @type Array
18187 */
18188 monthNames : [
18189 "January",
18190 "February",
18191 "March",
18192 "April",
18193 "May",
18194 "June",
18195 "July",
18196 "August",
18197 "September",
18198 "October",
18199 "November",
18200 "December"
18201 ],
18202
18203 /**
18204 * An object hash of zero-based JavaScript month numbers (with short month names as keys).
18205 *
18206 * __Note:__ keys are case-sensitive.
18207 *
18208 * Override these values for international dates.
18209 * Example:
18210 *
18211 * Ext.Date.monthNumbers = {
18212 * 'ShortJanNameInYourLang': 0,
18213 * 'ShortFebNameInYourLang': 1
18214 * // ...
18215 * };
18216 *
18217 * @type Object
18218 */
18219 monthNumbers : {
18220 Jan:0,
18221 Feb:1,
18222 Mar:2,
18223 Apr:3,
18224 May:4,
18225 Jun:5,
18226 Jul:6,
18227 Aug:7,
18228 Sep:8,
18229 Oct:9,
18230 Nov:10,
18231 Dec:11
18232 },
18233 /**
18234 * The date format string that the {@link Ext.util.Format#date} function uses.
18235 * See {@link Ext.Date} for details.
18236 *
18237 * This defaults to `m/d/Y`, but may be overridden in a locale file.
18238 * @property defaultFormat
18239 * @type String
18240 */
18241 defaultFormat : "m/d/Y",
18242 /**
18243 * Get the short month name for the given month number.
18244 * Override this function for international dates.
18245 * @param {Number} month A zero-based JavaScript month number.
18246 * @return {String} The short month name.
18247 */
18248 getShortMonthName : function(month) {
18249 return utilDate.monthNames[month].substring(0, 3);
18250 },
18251
18252 /**
18253 * Get the short day name for the given day number.
18254 * Override this function for international dates.
18255 * @param {Number} day A zero-based JavaScript day number.
18256 * @return {String} The short day name.
18257 */
18258 getShortDayName : function(day) {
18259 return utilDate.dayNames[day].substring(0, 3);
18260 },
18261
18262 /**
18263 * Get the zero-based JavaScript month number for the given short/full month name.
18264 * Override this function for international dates.
18265 * @param {String} name The short/full month name.
18266 * @return {Number} The zero-based JavaScript month number.
18267 */
18268 getMonthNumber : function(name) {
18269 // handle camel casing for English month names (since the keys for the Ext.Date.monthNumbers hash are case sensitive)
18270 return utilDate.monthNumbers[name.substring(0, 1).toUpperCase() + name.substring(1, 3).toLowerCase()];
18271 },
18272
18273 /**
18274 * The base format-code to formatting-function hashmap used by the {@link #format} method.
18275 * Formatting functions are strings (or functions which return strings) which
18276 * will return the appropriate value when evaluated in the context of the Date object
18277 * from which the {@link #format} method is called.
18278 * Add to / override these mappings for custom date formatting.
18279 *
18280 * __Note:__ `Ext.Date.format()` treats characters as literals if an appropriate mapping cannot be found.
18281 *
18282 * Example:
18283 *
18284 * @example
18285 * Ext.Date.formatCodes.x = "Ext.util.Format.leftPad(this.getDate(), 2, '0')";
18286 * alert(Ext.Date.format(new Date(), 'x')); // returns the current day of the month
18287 *
18288 * @type Object
18289 */
18290 formatCodes : {
18291 d: "Ext.String.leftPad(this.getDate(), 2, '0')",
18292 D: "Ext.Date.getShortDayName(this.getDay())", // get localized short day name
18293 j: "this.getDate()",
18294 l: "Ext.Date.dayNames[this.getDay()]",
18295 N: "(this.getDay() ? this.getDay() : 7)",
18296 S: "Ext.Date.getSuffix(this)",
18297 w: "this.getDay()",
18298 z: "Ext.Date.getDayOfYear(this)",
18299 W: "Ext.String.leftPad(Ext.Date.getWeekOfYear(this), 2, '0')",
18300 F: "Ext.Date.monthNames[this.getMonth()]",
18301 m: "Ext.String.leftPad(this.getMonth() + 1, 2, '0')",
18302 M: "Ext.Date.getShortMonthName(this.getMonth())", // get localized short month name
18303 n: "(this.getMonth() + 1)",
18304 t: "Ext.Date.getDaysInMonth(this)",
18305 L: "(Ext.Date.isLeapYear(this) ? 1 : 0)",
18306 o: "(this.getFullYear() + (Ext.Date.getWeekOfYear(this) == 1 && this.getMonth() > 0 ? +1 : (Ext.Date.getWeekOfYear(this) >= 52 && this.getMonth() < 11 ? -1 : 0)))",
18307 Y: "Ext.String.leftPad(this.getFullYear(), 4, '0')",
18308 y: "('' + this.getFullYear()).substring(2, 4)",
18309 a: "(this.getHours() < 12 ? 'am' : 'pm')",
18310 A: "(this.getHours() < 12 ? 'AM' : 'PM')",
18311 g: "((this.getHours() % 12) ? this.getHours() % 12 : 12)",
18312 G: "this.getHours()",
18313 h: "Ext.String.leftPad((this.getHours() % 12) ? this.getHours() % 12 : 12, 2, '0')",
18314 H: "Ext.String.leftPad(this.getHours(), 2, '0')",
18315 i: "Ext.String.leftPad(this.getMinutes(), 2, '0')",
18316 s: "Ext.String.leftPad(this.getSeconds(), 2, '0')",
18317 u: "Ext.String.leftPad(this.getMilliseconds(), 3, '0')",
18318 O: "Ext.Date.getGMTOffset(this)",
18319 P: "Ext.Date.getGMTOffset(this, true)",
18320 T: "Ext.Date.getTimezone(this)",
18321 Z: "(this.getTimezoneOffset() * -60)",
18322
18323 c: function() { // ISO-8601 -- GMT format
18324 for (var c = "Y-m-dTH:i:sP", code = [], i = 0, l = c.length; i < l; ++i) {
18325 var e = c.charAt(i);
18326 code.push(e == "T" ? "'T'" : utilDate.getFormatCode(e)); // treat T as a character literal
18327 }
18328 return code.join(" + ");
18329 },
18330 /*
18331 c: function() { // ISO-8601 -- UTC format
18332 return [
18333 "this.getUTCFullYear()", "'-'",
18334 "Ext.util.Format.leftPad(this.getUTCMonth() + 1, 2, '0')", "'-'",
18335 "Ext.util.Format.leftPad(this.getUTCDate(), 2, '0')",
18336 "'T'",
18337 "Ext.util.Format.leftPad(this.getUTCHours(), 2, '0')", "':'",
18338 "Ext.util.Format.leftPad(this.getUTCMinutes(), 2, '0')", "':'",
18339 "Ext.util.Format.leftPad(this.getUTCSeconds(), 2, '0')",
18340 "'Z'"
18341 ].join(" + ");
18342 },
18343 */
18344
18345 U: "Math.round(this.getTime() / 1000)"
18346 },
18347
18348 /**
18349 * Checks if the passed Date parameters will cause a JavaScript Date "rollover".
18350 * @param {Number} year 4-digit year.
18351 * @param {Number} month 1-based month-of-year.
18352 * @param {Number} day Day of month.
18353 * @param {Number} [hour] Hour.
18354 * @param {Number} [minute] Minute.
18355 * @param {Number} [second] Second.
18356 * @param {Number} [millisecond] Millisecond.
18357 * @return {Boolean} `true` if the passed parameters do not cause a Date "rollover", `false` otherwise.
18358 */
18359 isValid : function(y, m, d, h, i, s, ms) {
18360 // setup defaults
18361 h = h || 0;
18362 i = i || 0;
18363 s = s || 0;
18364 ms = ms || 0;
18365
18366 // Special handling for year < 100
18367 var dt = utilDate.add(new Date(y < 100 ? 100 : y, m - 1, d, h, i, s, ms), utilDate.YEAR, y < 100 ? y - 100 : 0);
18368
18369 return y == dt.getFullYear() &&
18370 m == dt.getMonth() + 1 &&
18371 d == dt.getDate() &&
18372 h == dt.getHours() &&
18373 i == dt.getMinutes() &&
18374 s == dt.getSeconds() &&
18375 ms == dt.getMilliseconds();
18376 },
18377
18378 /**
18379 * Parses the passed string using the specified date format.
18380 * Note that this function expects normal calendar dates, meaning that months are 1-based (i.e. 1 = January).
18381 * The {@link #defaults} hash will be used for any date value (i.e. year, month, day, hour, minute, second or millisecond)
18382 * which cannot be found in the passed string. If a corresponding default date value has not been specified in the {@link #defaults} hash,
18383 * the current date's year, month, day or DST-adjusted zero-hour time value will be used instead.
18384 * Keep in mind that the input date string must precisely match the specified format string
18385 * in order for the parse operation to be successful (failed parse operations return a `null` value).
18386 *
18387 * Example:
18388 *
18389 * // dt = Fri May 25 2007 (current date)
18390 * var dt = new Date();
18391 *
18392 * // dt = Thu May 25 2006 (today's month/day in 2006)
18393 * dt = Ext.Date.parse("2006", "Y");
18394 *
18395 * // dt = Sun Jan 15 2006 (all date parts specified)
18396 * dt = Ext.Date.parse("2006-01-15", "Y-m-d");
18397 *
18398 * // dt = Sun Jan 15 2006 15:20:01
18399 * dt = Ext.Date.parse("2006-01-15 3:20:01 PM", "Y-m-d g:i:s A");
18400 *
18401 * // attempt to parse Sun Feb 29 2006 03:20:01 in strict mode
18402 * dt = Ext.Date.parse("2006-02-29 03:20:01", "Y-m-d H:i:s", true); // null
18403 *
18404 * @param {String/Number} input The raw date string.
18405 * @param {String} format The expected date string format.
18406 * @param {Boolean} [strict=false] (optional) `true` to validate date strings while parsing (i.e. prevents JavaScript Date "rollover").
18407 * Invalid date strings will return `null` when parsed.
18408 * @return {Date/null} The parsed Date, or `null` if an invalid date string.
18409 */
18410 parse : function(input, format, strict) {
18411 var p = utilDate.parseFunctions;
18412 if (p[format] == null) {
18413 utilDate.createParser(format);
18414 }
18415 return p[format](input, Ext.isDefined(strict) ? strict : utilDate.useStrict);
18416 },
18417
18418 // Backwards compat
18419 parseDate: function(input, format, strict){
18420 return utilDate.parse(input, format, strict);
18421 },
18422
18423
18424 // @private
18425 getFormatCode : function(character) {
18426 var f = utilDate.formatCodes[character];
18427
18428 if (f) {
18429 f = typeof f == 'function'? f() : f;
18430 utilDate.formatCodes[character] = f; // reassign function result to prevent repeated execution
18431 }
18432
18433 // note: unknown characters are treated as literals
18434 return f || ("'" + Ext.String.escape(character) + "'");
18435 },
18436
18437 // @private
18438 createFormat : function(format) {
18439 var code = [],
18440 special = false,
18441 ch = '';
18442
18443 for (var i = 0; i < format.length; ++i) {
18444 ch = format.charAt(i);
18445 if (!special && ch == "\\") {
18446 special = true;
18447 } else if (special) {
18448 special = false;
18449 code.push("'" + Ext.String.escape(ch) + "'");
18450 } else if (ch == '\n') {
18451 code.push(Ext.JSON.encode(ch));
18452 } else {
18453 code.push(utilDate.getFormatCode(ch));
18454 }
18455 }
18456 utilDate.formatFunctions[format] = Ext.functionFactory("return " + code.join('+'));
18457 },
18458
18459 // @private
18460 createParser : (function() {
18461 var code = [
18462 "var dt, y, m, d, h, i, s, ms, o, z, zz, u, v,",
18463 "def = Ext.Date.defaults,",
18464 "results = String(input).match(Ext.Date.parseRegexes[{0}]);", // either null, or an array of matched strings
18465
18466 "if(results){",
18467 "{1}",
18468
18469 "if(u != null){", // i.e. unix time is defined
18470 "v = new Date(u * 1000);", // give top priority to UNIX time
18471 "}else{",
18472 // create Date object representing midnight of the current day;
18473 // this will provide us with our date defaults
18474 // (note: clearTime() handles Daylight Saving Time automatically)
18475 "dt = Ext.Date.clearTime(new Date);",
18476
18477 // date calculations (note: these calculations create a dependency on Ext.Number.from())
18478 "y = Ext.Number.from(y, Ext.Number.from(def.y, dt.getFullYear()));",
18479 "m = Ext.Number.from(m, Ext.Number.from(def.m - 1, dt.getMonth()));",
18480 "d = Ext.Number.from(d, Ext.Number.from(def.d, dt.getDate()));",
18481
18482 // time calculations (note: these calculations create a dependency on Ext.Number.from())
18483 "h = Ext.Number.from(h, Ext.Number.from(def.h, dt.getHours()));",
18484 "i = Ext.Number.from(i, Ext.Number.from(def.i, dt.getMinutes()));",
18485 "s = Ext.Number.from(s, Ext.Number.from(def.s, dt.getSeconds()));",
18486 "ms = Ext.Number.from(ms, Ext.Number.from(def.ms, dt.getMilliseconds()));",
18487
18488 "if(z >= 0 && y >= 0){",
18489 // both the year and zero-based day of year are defined and >= 0.
18490 // these 2 values alone provide sufficient info to create a full date object
18491
18492 // create Date object representing January 1st for the given year
18493 // handle years < 100 appropriately
18494 "v = Ext.Date.add(new Date(y < 100 ? 100 : y, 0, 1, h, i, s, ms), Ext.Date.YEAR, y < 100 ? y - 100 : 0);",
18495
18496 // then add day of year, checking for Date "rollover" if necessary
18497 "v = !strict? v : (strict === true && (z <= 364 || (Ext.Date.isLeapYear(v) && z <= 365))? Ext.Date.add(v, Ext.Date.DAY, z) : null);",
18498 "}else if(strict === true && !Ext.Date.isValid(y, m + 1, d, h, i, s, ms)){", // check for Date "rollover"
18499 "v = null;", // invalid date, so return null
18500 "}else{",
18501 // plain old Date object
18502 // handle years < 100 properly
18503 "v = Ext.Date.add(new Date(y < 100 ? 100 : y, m, d, h, i, s, ms), Ext.Date.YEAR, y < 100 ? y - 100 : 0);",
18504 "}",
18505 "}",
18506 "}",
18507
18508 "if(v){",
18509 // favor UTC offset over GMT offset
18510 "if(zz != null){",
18511 // reset to UTC, then add offset
18512 "v = Ext.Date.add(v, Ext.Date.SECOND, -v.getTimezoneOffset() * 60 - zz);",
18513 "}else if(o){",
18514 // reset to GMT, then add offset
18515 "v = Ext.Date.add(v, Ext.Date.MINUTE, -v.getTimezoneOffset() + (sn == '+'? -1 : 1) * (hr * 60 + mn));",
18516 "}",
18517 "}",
18518
18519 "return v;"
18520 ].join('\n');
18521
18522 return function(format) {
18523 var regexNum = utilDate.parseRegexes.length,
18524 currentGroup = 1,
18525 calc = [],
18526 regex = [],
18527 special = false,
18528 ch = "";
18529
18530 for (var i = 0; i < format.length; ++i) {
18531 ch = format.charAt(i);
18532 if (!special && ch == "\\") {
18533 special = true;
18534 } else if (special) {
18535 special = false;
18536 regex.push(Ext.String.escape(ch));
18537 } else {
18538 var obj = utilDate.formatCodeToRegex(ch, currentGroup);
18539 currentGroup += obj.g;
18540 regex.push(obj.s);
18541 if (obj.g && obj.c) {
18542 calc.push(obj.c);
18543 }
18544 }
18545 }
18546
18547 utilDate.parseRegexes[regexNum] = new RegExp("^" + regex.join('') + "$", 'i');
18548 utilDate.parseFunctions[format] = Ext.functionFactory("input", "strict", xf(code, regexNum, calc.join('')));
18549 };
18550 })(),
18551
18552 // @private
18553 parseCodes : {
18554 /*
18555 * Notes:
18556 * g = {Number} calculation group (0 or 1. only group 1 contributes to date calculations.)
18557 * c = {String} calculation method (required for group 1. null for group 0. {0} = currentGroup - position in regex result array)
18558 * s = {String} regex pattern. all matches are stored in results[], and are accessible by the calculation mapped to 'c'
18559 */
18560 d: {
18561 g:1,
18562 c:"d = parseInt(results[{0}], 10);\n",
18563 s:"(\\d{2})" // day of month with leading zeros (01 - 31)
18564 },
18565 j: {
18566 g:1,
18567 c:"d = parseInt(results[{0}], 10);\n",
18568 s:"(\\d{1,2})" // day of month without leading zeros (1 - 31)
18569 },
18570 D: function() {
18571 for (var a = [], i = 0; i < 7; a.push(utilDate.getShortDayName(i)), ++i); // get localized short day names
18572 return {
18573 g:0,
18574 c:null,
18575 s:"(?:" + a.join("|") +")"
18576 };
18577 },
18578 l: function() {
18579 return {
18580 g:0,
18581 c:null,
18582 s:"(?:" + utilDate.dayNames.join("|") + ")"
18583 };
18584 },
18585 N: {
18586 g:0,
18587 c:null,
18588 s:"[1-7]" // ISO-8601 day number (1 (monday) - 7 (sunday))
18589 },
18590 S: {
18591 g:0,
18592 c:null,
18593 s:"(?:st|nd|rd|th)"
18594 },
18595 w: {
18596 g:0,
18597 c:null,
18598 s:"[0-6]" // JavaScript day number (0 (sunday) - 6 (saturday))
18599 },
18600 z: {
18601 g:1,
18602 c:"z = parseInt(results[{0}], 10);\n",
18603 s:"(\\d{1,3})" // day of the year (0 - 364 (365 in leap years))
18604 },
18605 W: {
18606 g:0,
18607 c:null,
18608 s:"(?:\\d{2})" // ISO-8601 week number (with leading zero)
18609 },
18610 F: function() {
18611 return {
18612 g:1,
18613 c:"m = parseInt(Ext.Date.getMonthNumber(results[{0}]), 10);\n", // get localized month number
18614 s:"(" + utilDate.monthNames.join("|") + ")"
18615 };
18616 },
18617 M: function() {
18618 for (var a = [], i = 0; i < 12; a.push(utilDate.getShortMonthName(i)), ++i); // get localized short month names
18619 return Ext.applyIf({
18620 s:"(" + a.join("|") + ")"
18621 }, utilDate.formatCodeToRegex("F"));
18622 },
18623 m: {
18624 g:1,
18625 c:"m = parseInt(results[{0}], 10) - 1;\n",
18626 s:"(\\d{2})" // month number with leading zeros (01 - 12)
18627 },
18628 n: {
18629 g:1,
18630 c:"m = parseInt(results[{0}], 10) - 1;\n",
18631 s:"(\\d{1,2})" // month number without leading zeros (1 - 12)
18632 },
18633 t: {
18634 g:0,
18635 c:null,
18636 s:"(?:\\d{2})" // no. of days in the month (28 - 31)
18637 },
18638 L: {
18639 g:0,
18640 c:null,
18641 s:"(?:1|0)"
18642 },
18643 o: function() {
18644 return utilDate.formatCodeToRegex("Y");
18645 },
18646 Y: {
18647 g:1,
18648 c:"y = parseInt(results[{0}], 10);\n",
18649 s:"(\\d{4})" // 4-digit year
18650 },
18651 y: {
18652 g:1,
18653 c:"var ty = parseInt(results[{0}], 10);\n"
18654 + "y = ty > Ext.Date.y2kYear ? 1900 + ty : 2000 + ty;\n", // 2-digit year
18655 s:"(\\d{1,2})"
18656 },
18657 /*
18658 * In the am/pm parsing routines, we allow both upper and lower case
18659 * even though it doesn't exactly match the spec. It gives much more flexibility
18660 * in being able to specify case insensitive regexes.
18661 */
18662 a: {
18663 g:1,
18664 c:"if (/(am)/i.test(results[{0}])) {\n"
18665 + "if (!h || h == 12) { h = 0; }\n"
18666 + "} else { if (!h || h < 12) { h = (h || 0) + 12; }}",
18667 s:"(am|pm|AM|PM)"
18668 },
18669 A: {
18670 g:1,
18671 c:"if (/(am)/i.test(results[{0}])) {\n"
18672 + "if (!h || h == 12) { h = 0; }\n"
18673 + "} else { if (!h || h < 12) { h = (h || 0) + 12; }}",
18674 s:"(AM|PM|am|pm)"
18675 },
18676 g: function() {
18677 return utilDate.formatCodeToRegex("G");
18678 },
18679 G: {
18680 g:1,
18681 c:"h = parseInt(results[{0}], 10);\n",
18682 s:"(\\d{1,2})" // 24-hr format of an hour without leading zeros (0 - 23)
18683 },
18684 h: function() {
18685 return utilDate.formatCodeToRegex("H");
18686 },
18687 H: {
18688 g:1,
18689 c:"h = parseInt(results[{0}], 10);\n",
18690 s:"(\\d{2})" // 24-hr format of an hour with leading zeros (00 - 23)
18691 },
18692 i: {
18693 g:1,
18694 c:"i = parseInt(results[{0}], 10);\n",
18695 s:"(\\d{2})" // minutes with leading zeros (00 - 59)
18696 },
18697 s: {
18698 g:1,
18699 c:"s = parseInt(results[{0}], 10);\n",
18700 s:"(\\d{2})" // seconds with leading zeros (00 - 59)
18701 },
18702 u: {
18703 g:1,
18704 c:"ms = results[{0}]; ms = parseInt(ms, 10)/Math.pow(10, ms.length - 3);\n",
18705 s:"(\\d+)" // decimal fraction of a second (minimum = 1 digit, maximum = unlimited)
18706 },
18707 O: {
18708 g:1,
18709 c:[
18710 "o = results[{0}];",
18711 "var sn = o.substring(0,1),", // get + / - sign
18712 "hr = o.substring(1,3)*1 + Math.floor(o.substring(3,5) / 60),", // get hours (performs minutes-to-hour conversion also, just in case)
18713 "mn = o.substring(3,5) % 60;", // get minutes
18714 "o = ((-12 <= (hr*60 + mn)/60) && ((hr*60 + mn)/60 <= 14))? (sn + Ext.String.leftPad(hr, 2, '0') + Ext.String.leftPad(mn, 2, '0')) : null;\n" // -12hrs <= GMT offset <= 14hrs
18715 ].join("\n"),
18716 s: "([+\-]\\d{4})" // GMT offset in hrs and mins
18717 },
18718 P: {
18719 g:1,
18720 c:[
18721 "o = results[{0}];",
18722 "var sn = o.substring(0,1),", // get + / - sign
18723 "hr = o.substring(1,3)*1 + Math.floor(o.substring(4,6) / 60),", // get hours (performs minutes-to-hour conversion also, just in case)
18724 "mn = o.substring(4,6) % 60;", // get minutes
18725 "o = ((-12 <= (hr*60 + mn)/60) && ((hr*60 + mn)/60 <= 14))? (sn + Ext.String.leftPad(hr, 2, '0') + Ext.String.leftPad(mn, 2, '0')) : null;\n" // -12hrs <= GMT offset <= 14hrs
18726 ].join("\n"),
18727 s: "([+\-]\\d{2}:\\d{2})" // GMT offset in hrs and mins (with colon separator)
18728 },
18729 T: {
18730 g:0,
18731 c:null,
18732 s:"[A-Z]{1,4}" // timezone abbrev. may be between 1 - 4 chars
18733 },
18734 Z: {
18735 g:1,
18736 c:"zz = results[{0}] * 1;\n" // -43200 <= UTC offset <= 50400
18737 + "zz = (-43200 <= zz && zz <= 50400)? zz : null;\n",
18738 s:"([+\-]?\\d{1,5})" // leading '+' sign is optional for UTC offset
18739 },
18740 c: function() {
18741 var calc = [],
18742 arr = [
18743 utilDate.formatCodeToRegex("Y", 1), // year
18744 utilDate.formatCodeToRegex("m", 2), // month
18745 utilDate.formatCodeToRegex("d", 3), // day
18746 utilDate.formatCodeToRegex("h", 4), // hour
18747 utilDate.formatCodeToRegex("i", 5), // minute
18748 utilDate.formatCodeToRegex("s", 6), // second
18749 {c:"ms = results[7] || '0'; ms = parseInt(ms, 10)/Math.pow(10, ms.length - 3);\n"}, // decimal fraction of a second (minimum = 1 digit, maximum = unlimited)
18750 {c:[ // allow either "Z" (i.e. UTC) or "-0530" or "+08:00" (i.e. UTC offset) timezone delimiters. assumes local timezone if no timezone is specified
18751 "if(results[8]) {", // timezone specified
18752 "if(results[8] == 'Z'){",
18753 "zz = 0;", // UTC
18754 "}else if (results[8].indexOf(':') > -1){",
18755 utilDate.formatCodeToRegex("P", 8).c, // timezone offset with colon separator
18756 "}else{",
18757 utilDate.formatCodeToRegex("O", 8).c, // timezone offset without colon separator
18758 "}",
18759 "}"
18760 ].join('\n')}
18761 ];
18762
18763 for (var i = 0, l = arr.length; i < l; ++i) {
18764 calc.push(arr[i].c);
18765 }
18766
18767 return {
18768 g:1,
18769 c:calc.join(""),
18770 s:[
18771 arr[0].s, // year (required)
18772 "(?:", "-", arr[1].s, // month (optional)
18773 "(?:", "-", arr[2].s, // day (optional)
18774 "(?:",
18775 "(?:T| )?", // time delimiter -- either a "T" or a single blank space
18776 arr[3].s, ":", arr[4].s, // hour AND minute, delimited by a single colon (optional). MUST be preceded by either a "T" or a single blank space
18777 "(?::", arr[5].s, ")?", // seconds (optional)
18778 "(?:(?:\\.|,)(\\d+))?", // decimal fraction of a second (e.g. ",12345" or ".98765") (optional)
18779 "(Z|(?:[-+]\\d{2}(?::)?\\d{2}))?", // "Z" (UTC) or "-0530" (UTC offset without colon delimiter) or "+08:00" (UTC offset with colon delimiter) (optional)
18780 ")?",
18781 ")?",
18782 ")?"
18783 ].join("")
18784 };
18785 },
18786 U: {
18787 g:1,
18788 c:"u = parseInt(results[{0}], 10);\n",
18789 s:"(-?\\d+)" // leading minus sign indicates seconds before UNIX epoch
18790 }
18791 },
18792
18793 // Old Ext.Date prototype methods.
18794 // @private
18795 dateFormat: function(date, format) {
18796 return utilDate.format(date, format);
18797 },
18798
18799 /**
18800 * Formats a date given the supplied format string.
18801 * @param {Date} date The date to format.
18802 * @param {String} format The format string.
18803 * @return {String} The formatted date.
18804 */
18805 format: function(date, format) {
18806 if (utilDate.formatFunctions[format] == null) {
18807 utilDate.createFormat(format);
18808 }
18809 var result = utilDate.formatFunctions[format].call(date);
18810 return result + '';
18811 },
18812
18813 /**
18814 * Get the timezone abbreviation of the current date (equivalent to the format specifier 'T').
18815 *
18816 * __Note:__ The date string returned by the JavaScript Date object's `toString()` method varies
18817 * between browsers (e.g. FF vs IE) and system region settings (e.g. IE in Asia vs IE in America).
18818 * For a given date string e.g. "Thu Oct 25 2007 22:55:35 GMT+0800 (Malay Peninsula Standard Time)",
18819 * `getTimezone()` first tries to get the timezone abbreviation from between a pair of parentheses
18820 * (which may or may not be present), failing which it proceeds to get the timezone abbreviation
18821 * from the GMT offset portion of the date string.
18822 *
18823 * @example
18824 * var dt = new Date('9/17/2011');
18825 * alert(Ext.Date.getTimezone(dt));
18826 *
18827 * @param {Date} date The date.
18828 * @return {String} The abbreviated timezone name (e.g. 'CST', 'PDT', 'EDT', 'MPST' ...).
18829 */
18830 getTimezone : function(date) {
18831 // the following list shows the differences between date strings from different browsers on a WinXP SP2 machine from an Asian locale:
18832 //
18833 // Opera : "Thu, 25 Oct 2007 22:53:45 GMT+0800" -- shortest (weirdest) date string of the lot
18834 // Safari : "Thu Oct 25 2007 22:55:35 GMT+0800 (Malay Peninsula Standard Time)" -- value in parentheses always gives the correct timezone (same as FF)
18835 // FF : "Thu Oct 25 2007 22:55:35 GMT+0800 (Malay Peninsula Standard Time)" -- value in parentheses always gives the correct timezone
18836 // IE : "Thu Oct 25 22:54:35 UTC+0800 2007" -- (Asian system setting) look for 3-4 letter timezone abbrev
18837 // IE : "Thu Oct 25 17:06:37 PDT 2007" -- (American system setting) look for 3-4 letter timezone abbrev
18838 //
18839 // this crazy regex attempts to guess the correct timezone abbreviation despite these differences.
18840 // step 1: (?:\((.*)\) -- find timezone in parentheses
18841 // step 2: ([A-Z]{1,4})(?:[\-+][0-9]{4})?(?: -?\d+)?) -- if nothing was found in step 1, find timezone from timezone offset portion of date string
18842 // step 3: remove all non uppercase characters found in step 1 and 2
18843 return date.toString().replace(/^.* (?:\((.*)\)|([A-Z]{1,4})(?:[\-+][0-9]{4})?(?: -?\d+)?)$/, "$1$2").replace(/[^A-Z]/g, "");
18844 },
18845
18846 /**
18847 * Get the offset from GMT of the current date (equivalent to the format specifier 'O').
18848 *
18849 * @example
18850 * var dt = new Date('9/17/2011');
18851 * alert(Ext.Date.getGMTOffset(dt));
18852 *
18853 * @param {Date} date The date.
18854 * @param {Boolean} [colon=false] (optional) `true` to separate the hours and minutes with a colon.
18855 * @return {String} The 4-character offset string prefixed with + or - (e.g. '-0600').
18856 */
18857 getGMTOffset : function(date, colon) {
18858 var offset = date.getTimezoneOffset();
18859 return (offset > 0 ? "-" : "+")
18860 + Ext.String.leftPad(Math.floor(Math.abs(offset) / 60), 2, "0")
18861 + (colon ? ":" : "")
18862 + Ext.String.leftPad(Math.abs(offset % 60), 2, "0");
18863 },
18864
18865 /**
18866 * Get the numeric day number of the year, adjusted for leap year.
18867 *
18868 * @example
18869 * var dt = new Date('9/17/2011');
18870 * alert(Ext.Date.getDayOfYear(dt)); // 259
18871 *
18872 * @param {Date} date The date.
18873 * @return {Number} 0 to 364 (365 in leap years).
18874 */
18875 getDayOfYear: function(date) {
18876 var num = 0,
18877 d = Ext.Date.clone(date),
18878 m = date.getMonth(),
18879 i;
18880
18881 for (i = 0, d.setDate(1), d.setMonth(0); i < m; d.setMonth(++i)) {
18882 num += utilDate.getDaysInMonth(d);
18883 }
18884 return num + date.getDate() - 1;
18885 },
18886
18887 /**
18888 * Get the numeric ISO-8601 week number of the year
18889 * (equivalent to the format specifier 'W', but without a leading zero).
18890 *
18891 * @example
18892 * var dt = new Date('9/17/2011');
18893 * alert(Ext.Date.getWeekOfYear(dt)); // 37
18894 *
18895 * @param {Date} date The date.
18896 * @return {Number} 1 to 53.
18897 * @method
18898 */
18899 getWeekOfYear : (function() {
18900 // adapted from http://www.merlyn.demon.co.uk/weekcalc.htm
18901 var ms1d = 864e5, // milliseconds in a day
18902 ms7d = 7 * ms1d; // milliseconds in a week
18903
18904 return function(date) { // return a closure so constants get calculated only once
18905 var DC3 = Date.UTC(date.getFullYear(), date.getMonth(), date.getDate() + 3) / ms1d, // an Absolute Day Number
18906 AWN = Math.floor(DC3 / 7), // an Absolute Week Number
18907 Wyr = new Date(AWN * ms7d).getUTCFullYear();
18908
18909 return AWN - Math.floor(Date.UTC(Wyr, 0, 7) / ms7d) + 1;
18910 };
18911 })(),
18912
18913 /**
18914 * Checks if the current date falls within a leap year.
18915 *
18916 * @example
18917 * var dt = new Date('1/10/2011');
18918 * alert(Ext.Date.isLeapYear(dt)); // false
18919 *
18920 * @param {Date} date The date.
18921 * @return {Boolean} `true` if the current date falls within a leap year, `false` otherwise.
18922 */
18923 isLeapYear : function(date) {
18924 var year = date.getFullYear();
18925 return !!((year & 3) == 0 && (year % 100 || (year % 400 == 0 && year)));
18926 },
18927
18928 /**
18929 * Get the first day of the current month, adjusted for leap year. The returned value
18930 * is the numeric day index within the week (0-6) which can be used in conjunction with
18931 * the {@link #monthNames} array to retrieve the textual day name.
18932 *
18933 * @example
18934 * var dt = new Date('1/10/2007'),
18935 * firstDay = Ext.Date.getFirstDayOfMonth(dt);
18936 * alert(Ext.Date.dayNames[firstDay]); // 'Monday'
18937 *
18938 * @param {Date} date The date
18939 * @return {Number} The day number (0-6).
18940 */
18941 getFirstDayOfMonth : function(date) {
18942 var day = (date.getDay() - (date.getDate() - 1)) % 7;
18943 return (day < 0) ? (day + 7) : day;
18944 },
18945
18946 /**
18947 * Get the last day of the current month, adjusted for leap year. The returned value
18948 * is the numeric day index within the week (0-6) which can be used in conjunction with
18949 * the {@link #monthNames} array to retrieve the textual day name.
18950 *
18951 * @example
18952 * var dt = new Date('1/10/2007'),
18953 * lastDay = Ext.Date.getLastDayOfMonth(dt);
18954 * alert(Ext.Date.dayNames[lastDay]); // 'Wednesday'
18955 *
18956 * @param {Date} date The date.
18957 * @return {Number} The day number (0-6).
18958 */
18959 getLastDayOfMonth : function(date) {
18960 return utilDate.getLastDateOfMonth(date).getDay();
18961 },
18962
18963
18964 /**
18965 * Get the date of the first day of the month in which this date resides.
18966 *
18967 * @example
18968 * var dt = new Date('1/10/2007'),
18969 * lastDate = Ext.Date.getFirstDateOfMonth(dt);
18970 * alert(lastDate); // Mon Jan 01 2007 00:00:00 GMT-0800 (PST)
18971 *
18972 * @param {Date} date The date.
18973 * @return {Date}
18974 */
18975 getFirstDateOfMonth : function(date) {
18976 return new Date(date.getFullYear(), date.getMonth(), 1);
18977 },
18978
18979 /**
18980 * Get the date of the last day of the month in which this date resides.
18981 *
18982 * @example
18983 * var dt = new Date('1/10/2007'),
18984 * lastDate = Ext.Date.getLastDateOfMonth(dt);
18985 * alert(lastDate); // Wed Jan 31 2007 00:00:00 GMT-0800 (PST)
18986 *
18987 * @param {Date} date The date.
18988 * @return {Date}
18989 */
18990 getLastDateOfMonth : function(date) {
18991 return new Date(date.getFullYear(), date.getMonth(), utilDate.getDaysInMonth(date));
18992 },
18993
18994 /**
18995 * Get the number of days in the current month, adjusted for leap year.
18996 *
18997 * @example
18998 * var dt = new Date('1/10/2007');
18999 * alert(Ext.Date.getDaysInMonth(dt)); // 31
19000 *
19001 * @param {Date} date The date.
19002 * @return {Number} The number of days in the month.
19003 * @method
19004 */
19005 getDaysInMonth: (function() {
19006 var daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
19007
19008 return function(date) { // return a closure for efficiency
19009 var m = date.getMonth();
19010
19011 return m == 1 && utilDate.isLeapYear(date) ? 29 : daysInMonth[m];
19012 };
19013 })(),
19014
19015 /**
19016 * Get the English ordinal suffix of the current day (equivalent to the format specifier 'S').
19017 *
19018 * @example
19019 * var dt = new Date('9/17/2011');
19020 * alert(Ext.Date.getSuffix(dt)); // 'th'
19021 *
19022 * @param {Date} date The date.
19023 * @return {String} 'st', 'nd', 'rd' or 'th'.
19024 */
19025 getSuffix : function(date) {
19026 switch (date.getDate()) {
19027 case 1:
19028 case 21:
19029 case 31:
19030 return "st";
19031 case 2:
19032 case 22:
19033 return "nd";
19034 case 3:
19035 case 23:
19036 return "rd";
19037 default:
19038 return "th";
19039 }
19040 },
19041
19042 /**
19043 * Creates and returns a new Date instance with the exact same date value as the called instance.
19044 * Dates are copied and passed by reference, so if a copied date variable is modified later, the original
19045 * variable will also be changed. When the intention is to create a new variable that will not
19046 * modify the original instance, you should create a clone.
19047 *
19048 * Example of correctly cloning a date:
19049 *
19050 * // wrong way:
19051 * var orig = new Date('10/1/2006');
19052 * var copy = orig;
19053 * copy.setDate(5);
19054 * console.log(orig); // returns 'Thu Oct 05 2006'!
19055 *
19056 * // correct way:
19057 * var orig = new Date('10/1/2006'),
19058 * copy = Ext.Date.clone(orig);
19059 * copy.setDate(5);
19060 * console.log(orig); // returns 'Thu Oct 01 2006'
19061 *
19062 * @param {Date} date The date.
19063 * @return {Date} The new Date instance.
19064 */
19065 clone : function(date) {
19066 return new Date(date.getTime());
19067 },
19068
19069 /**
19070 * Checks if the current date is affected by Daylight Saving Time (DST).
19071 *
19072 * @example
19073 * var dt = new Date('9/17/2011');
19074 * alert(Ext.Date.isDST(dt));
19075 *
19076 * @param {Date} date The date.
19077 * @return {Boolean} `true` if the current date is affected by DST.
19078 */
19079 isDST : function(date) {
19080 // adapted from http://sencha.com/forum/showthread.php?p=247172#post247172
19081 // courtesy of @geoffrey.mcgill
19082 return new Date(date.getFullYear(), 0, 1).getTimezoneOffset() != date.getTimezoneOffset();
19083 },
19084
19085 /**
19086 * Attempts to clear all time information from this Date by setting the time to midnight of the same day,
19087 * automatically adjusting for Daylight Saving Time (DST) where applicable.
19088 *
19089 * __Note:__ DST timezone information for the browser's host operating system is assumed to be up-to-date.
19090 *
19091 * @param {Date} date The date.
19092 * @param {Boolean} [clone=false] `true` to create a clone of this date, clear the time and return it.
19093 * @return {Date} this or the clone.
19094 */
19095 clearTime : function(date, clone) {
19096 if (clone) {
19097 return Ext.Date.clearTime(Ext.Date.clone(date));
19098 }
19099
19100 // get current date before clearing time
19101 var d = date.getDate();
19102
19103 // clear time
19104 date.setHours(0);
19105 date.setMinutes(0);
19106 date.setSeconds(0);
19107 date.setMilliseconds(0);
19108
19109 if (date.getDate() != d) { // account for DST (i.e. day of month changed when setting hour = 0)
19110 // note: DST adjustments are assumed to occur in multiples of 1 hour (this is almost always the case)
19111 // refer to http://www.timeanddate.com/time/aboutdst.html for the (rare) exceptions to this rule
19112
19113 // increment hour until cloned date == current date
19114 for (var hr = 1, c = utilDate.add(date, Ext.Date.HOUR, hr); c.getDate() != d; hr++, c = utilDate.add(date, Ext.Date.HOUR, hr));
19115
19116 date.setDate(d);
19117 date.setHours(c.getHours());
19118 }
19119
19120 return date;
19121 },
19122
19123 /**
19124 * Provides a convenient method for performing basic date arithmetic. This method
19125 * does not modify the Date instance being called - it creates and returns
19126 * a new Date instance containing the resulting date value.
19127 *
19128 * @example
19129 * // Basic usage:
19130 * var dt = Ext.Date.add(new Date('10/29/2006'), Ext.Date.DAY, 5);
19131 * alert(dt); // 'Fri Nov 03 2006 00:00:00'
19132 *
19133 * You can also subtract date values by passing a negative value:
19134 *
19135 * @example
19136 * // Negative values will be subtracted:
19137 * var dt2 = Ext.Date.add(new Date('10/1/2006'), Ext.Date.DAY, -5);
19138 * alert(dt2); // 'Tue Sep 26 2006 00:00:00'
19139 *
19140 * @param {Date} date The date to modify.
19141 * @param {String} interval A valid date interval enum value.
19142 * @param {Number} value The amount to add to the current date.
19143 * @return {Date} The new Date instance.
19144 */
19145 add : function(date, interval, value) {
19146 var d = Ext.Date.clone(date);
19147 if (!interval || value === 0) return d;
19148
19149 switch(interval.toLowerCase()) {
19150 case Ext.Date.MILLI:
19151 d= new Date(d.valueOf() + value);
19152 break;
19153 case Ext.Date.SECOND:
19154 d= new Date(d.valueOf() + value * 1000);
19155 break;
19156 case Ext.Date.MINUTE:
19157 d= new Date(d.valueOf() + value * 60000);
19158 break;
19159 case Ext.Date.HOUR:
19160 d= new Date(d.valueOf() + value * 3600000);
19161 break;
19162 case Ext.Date.DAY:
19163 d= new Date(d.valueOf() + value * 86400000);
19164 break;
19165 case Ext.Date.MONTH:
19166 var day = date.getDate();
19167 if (day > 28) {
19168 day = Math.min(day, Ext.Date.getLastDateOfMonth(Ext.Date.add(Ext.Date.getFirstDateOfMonth(date), 'mo', value)).getDate());
19169 }
19170 d.setDate(day);
19171 d.setMonth(date.getMonth() + value);
19172 break;
19173 case Ext.Date.YEAR:
19174 d.setFullYear(date.getFullYear() + value);
19175 break;
19176 }
19177 return d;
19178 },
19179
19180 /**
19181 * Checks if a date falls on or between the given start and end dates.
19182 * @param {Date} date The date to check.
19183 * @param {Date} start Start date.
19184 * @param {Date} end End date.
19185 * @return {Boolean} `true` if this date falls on or between the given start and end dates.
19186 */
19187 between : function(date, start, end) {
19188 var t = date.getTime();
19189 return start.getTime() <= t && t <= end.getTime();
19190 },
19191
19192 /**
19193 * Calculate how many units are there between two time.
19194 * @param {Date} min The first time.
19195 * @param {Date} max The second time.
19196 * @param {String} unit The unit. This unit is compatible with the date interval constants.
19197 * @return {Number} The maximum number n of units that min + n * unit <= max.
19198 */
19199 diff: function (min, max, unit) {
19200 var ExtDate = Ext.Date, est, diff = +max - min;
19201 switch (unit) {
19202 case ExtDate.MILLI:
19203 return diff;
19204 case ExtDate.SECOND:
19205 return Math.floor(diff / 1000);
19206 case ExtDate.MINUTE:
19207 return Math.floor(diff / 60000);
19208 case ExtDate.HOUR:
19209 return Math.floor(diff / 3600000);
19210 case ExtDate.DAY:
19211 return Math.floor(diff / 86400000);
19212 case 'w':
19213 return Math.floor(diff / 604800000);
19214 case ExtDate.MONTH:
19215 est = (max.getFullYear() * 12 + max.getMonth()) - (min.getFullYear() * 12 + min.getMonth());
19216 if (Ext.Date.add(min, unit, est) > max) {
19217 return est - 1;
19218 } else {
19219 return est;
19220 }
19221 case ExtDate.YEAR:
19222 est = max.getFullYear() - min.getFullYear();
19223 if (Ext.Date.add(min, unit, est) > max) {
19224 return est - 1;
19225 } else {
19226 return est;
19227 }
19228 }
19229 },
19230
19231 /**
19232 * Align the date to `unit`.
19233 * @param {Date} date The date to be aligned.
19234 * @param {String} unit The unit. This unit is compatible with the date interval constants.
19235 * @return {Date} The aligned date.
19236 */
19237 align: function (date, unit, step) {
19238 var num = new Date(+date);
19239 switch (unit.toLowerCase()) {
19240 case Ext.Date.MILLI:
19241 return num;
19242 break;
19243 case Ext.Date.SECOND:
19244 num.setUTCSeconds(num.getUTCSeconds() - num.getUTCSeconds() % step);
19245 num.setUTCMilliseconds(0);
19246 return num;
19247 break;
19248 case Ext.Date.MINUTE:
19249 num.setUTCMinutes(num.getUTCMinutes() - num.getUTCMinutes() % step);
19250 num.setUTCSeconds(0);
19251 num.setUTCMilliseconds(0);
19252 return num;
19253 break;
19254 case Ext.Date.HOUR:
19255 num.setUTCHours(num.getUTCHours() - num.getUTCHours() % step);
19256 num.setUTCMinutes(0);
19257 num.setUTCSeconds(0);
19258 num.setUTCMilliseconds(0);
19259 return num;
19260 break;
19261 case Ext.Date.DAY:
19262 if (step == 7 || step == 14){
19263 num.setUTCDate(num.getUTCDate() - num.getUTCDay() + 1);
19264 }
19265 num.setUTCHours(0);
19266 num.setUTCMinutes(0);
19267 num.setUTCSeconds(0);
19268 num.setUTCMilliseconds(0);
19269 return num;
19270 break;
19271 case Ext.Date.MONTH:
19272 num.setUTCMonth(num.getUTCMonth() - (num.getUTCMonth() - 1) % step,1);
19273 num.setUTCHours(0);
19274 num.setUTCMinutes(0);
19275 num.setUTCSeconds(0);
19276 num.setUTCMilliseconds(0);
19277 return num;
19278 break;
19279 case Ext.Date.YEAR:
19280 num.setUTCFullYear(num.getUTCFullYear() - num.getUTCFullYear() % step, 1, 1);
19281 num.setUTCHours(0);
19282 num.setUTCMinutes(0);
19283 num.setUTCSeconds(0);
19284 num.setUTCMilliseconds(0);
19285 return date;
19286 break;
19287 }
19288 }
19289 };
19290
19291 var utilDate = Ext.DateExtras;
19292
19293 Ext.apply(Ext.Date, utilDate);
19294
19295
19296 })();
19297
19298
19299
19300 /**
19301 * Reusable data formatting functions
19302 */
19303 Ext.define('Ext.util.Format', {
19304
19305
19306
19307
19308 singleton: true,
19309
19310 /**
19311 * The global default date format.
19312 */
19313 defaultDateFormat: 'm/d/Y',
19314
19315 escapeRe: /('|\\)/g,
19316 trimRe: /^[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000]+|[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000]+$/g,
19317 formatRe: /\{(\d+)\}/g,
19318 escapeRegexRe: /([-.*+?^${}()|[\]\/\\])/g,
19319 dashesRe: /-/g,
19320 iso8601TestRe: /\d\dT\d\d/,
19321 iso8601SplitRe: /[- :T\.Z\+]/,
19322
19323 /**
19324 * Truncate a string and add an ellipsis ('...') to the end if it exceeds the specified length.
19325 * @param {String} value The string to truncate.
19326 * @param {Number} length The maximum length to allow before truncating.
19327 * @param {Boolean} [word=false] True to try to find a common word break.
19328 * @return {String} The converted text.
19329 */
19330 ellipsis: function(value, len, word) {
19331 if (value && value.length > len) {
19332 if (word) {
19333 var vs = value.substr(0, len - 2),
19334 index = Math.max(vs.lastIndexOf(' '), vs.lastIndexOf('.'), vs.lastIndexOf('!'), vs.lastIndexOf('?'));
19335 if (index != -1 && index >= (len - 15)) {
19336 return vs.substr(0, index) + "...";
19337 }
19338 }
19339 return value.substr(0, len - 3) + "...";
19340 }
19341 return value;
19342 },
19343
19344 /**
19345 * Escapes the passed string for use in a regular expression.
19346 * @param {String} str
19347 * @return {String}
19348 */
19349 escapeRegex: function(s) {
19350 return s.replace(Ext.util.Format.escapeRegexRe, "\\$1");
19351 },
19352
19353 /**
19354 * Escapes the passed string for ' and \.
19355 * @param {String} string The string to escape.
19356 * @return {String} The escaped string.
19357 */
19358 escape: function(string) {
19359 return string.replace(Ext.util.Format.escapeRe, "\\$1");
19360 },
19361
19362 /**
19363 * Utility function that allows you to easily switch a string between two alternating values. The passed value
19364 * is compared to the current string, and if they are equal, the other value that was passed in is returned. If
19365 * they are already different, the first value passed in is returned.
19366 *
19367 * __Note:__ This method returns the new value but does not change the current string.
19368 *
19369 * // alternate sort directions
19370 * sort = Ext.util.Format.toggle(sort, 'ASC', 'DESC');
19371 *
19372 * // instead of conditional logic:
19373 * sort = (sort === 'ASC' ? 'DESC' : 'ASC');
19374 *
19375 * @param {String} string The current string
19376 * @param {String} value The value to compare to the current string
19377 * @param {String} other The new value to use if the string already equals the first value passed in
19378 * @return {String} The new value
19379 */
19380 toggle: function(string, value, other) {
19381 return string == value ? other : value;
19382 },
19383
19384 /**
19385 * Trims whitespace from either end of a string, leaving spaces within the string intact. Example:
19386 *
19387 * var s = ' foo bar ';
19388 * alert('-' + s + '-'); // alerts "- foo bar -"
19389 * alert('-' + Ext.util.Format.trim(s) + '-'); // alerts "-foo bar-"
19390 *
19391 * @param {String} string The string to escape
19392 * @return {String} The trimmed string
19393 */
19394 trim: function(string) {
19395 return string.replace(Ext.util.Format.trimRe, "");
19396 },
19397
19398 /**
19399 * Pads the left side of a string with a specified character. This is especially useful
19400 * for normalizing number and date strings. Example usage:
19401 *
19402 * var s = Ext.util.Format.leftPad('123', 5, '0');
19403 * // s now contains the string: '00123'
19404 *
19405 * @param {String} string The original string.
19406 * @param {Number} size The total length of the output string.
19407 * @param {String} [char=' '] (optional) The character with which to pad the original string.
19408 * @return {String} The padded string.
19409 */
19410 leftPad: function (val, size, ch) {
19411 var result = String(val);
19412 ch = ch || " ";
19413 while (result.length < size) {
19414 result = ch + result;
19415 }
19416 return result;
19417 },
19418
19419 /**
19420 * Allows you to define a tokenized string and pass an arbitrary number of arguments to replace the tokens. Each
19421 * token must be unique, and must increment in the format {0}, {1}, etc. Example usage:
19422 *
19423 * var cls = 'my-class', text = 'Some text';
19424 * var s = Ext.util.Format.format('<div class="{0}">{1}</div>', cls, text);
19425 * // s now contains the string: '<div class="my-class">Some text</div>'
19426 *
19427 * @param {String} string The tokenized string to be formatted.
19428 * @param {String...} values The values to replace token {0}, {1}, etc.
19429 * @return {String} The formatted string.
19430 */
19431 format: function (format) {
19432 var args = Ext.toArray(arguments, 1);
19433 return format.replace(Ext.util.Format.formatRe, function(m, i) {
19434 return args[i];
19435 });
19436 },
19437
19438 /**
19439 * Convert certain characters (&, <, >, and ') to their HTML character equivalents for literal display in web pages.
19440 * @param {String} value The string to encode.
19441 * @return {String} The encoded text.
19442 */
19443 htmlEncode: function(value) {
19444 return ! value ? value: String(value).replace(/&/g, "&amp;").replace(/>/g, "&gt;").replace(/</g, "&lt;").replace(/"/g, "&quot;");
19445 },
19446
19447 /**
19448 * Convert certain characters (&, <, >, and ') from their HTML character equivalents.
19449 * @param {String} value The string to decode.
19450 * @return {String} The decoded text.
19451 */
19452 htmlDecode: function(value) {
19453 return ! value ? value: String(value).replace(/&gt;/g, ">").replace(/&lt;/g, "<").replace(/&quot;/g, '"').replace(/&amp;/g, "&");
19454 },
19455
19456 /**
19457 * Parse a value into a formatted date using the specified format pattern.
19458 * Note that this uses the native Javascript Date.parse() method and is therefore subject to its idiosyncrasies.
19459 * Most formats assume the local timezone unless specified. One notable exception is 'YYYY-MM-DD' (note the dashes)
19460 * which is typically interpreted in UTC and can cause date shifting.
19461 * @param {String/Date} value The value to format. Strings must conform to the format expected by the JavaScript
19462 * Date object's [parse() method](http://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/parse).
19463 * @param {String} [format='m/d/Y'] (optional) Any valid date format string.
19464 * @return {String} The formatted date string.
19465 */
19466 date: function(value, format) {
19467 var date = value;
19468 if (!value) {
19469 return "";
19470 }
19471 if (!Ext.isDate(value)) {
19472 date = new Date(Date.parse(value));
19473 if (isNaN(date)) {
19474 // Dates with ISO 8601 format are not well supported by mobile devices, this can work around the issue.
19475 if (this.iso8601TestRe.test(value)) {
19476 // Fix for older android browsers to properly implement ISO 8601 formatted dates with timezone
19477 if (Ext.os.is.Android && Ext.os.version.isLessThan("3.0")) {
19478 /**
19479 * This code is modified from the following source: <https://github.com/csnover/js-iso8601>
19480 * © 2011 Colin Snover <http://zetafleet.com>
19481 * Released under MIT license.
19482 */
19483 var potentialUndefinedKeys = [ 1, 4, 5, 6, 7, 10, 11 ];
19484 var dateParsed, minutesOffset = 0;
19485
19486 // Capture Groups
19487 // 1 YYYY (optional)
19488 // 2 MM
19489 // 3 DD
19490 // 4 HH
19491 // 5 mm (optional)
19492 // 6 ss (optional)
19493 // 7 msec (optional)
19494 // 8 Z (optional)
19495 // 9 ± (optional)
19496 // 10 tzHH (optional)
19497 // 11 tzmm (optional)
19498 if ((dateParsed = /^(\d{4}|[+\-]\d{6})(?:-(\d{2})(?:-(\d{2}))?)?(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{3}))?)?(?:(Z)|([+\-])(\d{2})(?::(\d{2}))?)?)?$/.exec(value))) {
19499
19500 //Set any undefined values needed for Date to 0
19501 for (var i = 0, k; (k = potentialUndefinedKeys[i]); ++i) {
19502 dateParsed[k] = +dateParsed[k] || 0;
19503 }
19504
19505 // Fix undefined month and decrement
19506 dateParsed[2] = (+dateParsed[2] || 1) - 1;
19507 //fix undefined days
19508 dateParsed[3] = +dateParsed[3] || 1;
19509
19510 // Correct for timezone
19511 if (dateParsed[8] !== 'Z' && dateParsed[9] !== undefined) {
19512 minutesOffset = dateParsed[10] * 60 + dateParsed[11];
19513
19514 if (dateParsed[9] === '+') {
19515 minutesOffset = 0 - minutesOffset;
19516 }
19517 }
19518
19519 // Calculate valid date
19520 date = new Date(Date.UTC(dateParsed[1], dateParsed[2], dateParsed[3], dateParsed[4], dateParsed[5] + minutesOffset, dateParsed[6], dateParsed[7]));
19521 }
19522 } else {
19523 date = value.split(this.iso8601SplitRe);
19524 date = new Date(date[0], date[1] - 1, date[2], date[3], date[4], date[5]);
19525 }
19526 }
19527 }
19528 if (isNaN(date)) {
19529 // Dates with the format "2012-01-20" fail, but "2012/01/20" work in some browsers. We'll try and
19530 // get around that.
19531 date = new Date(Date.parse(value.replace(this.dashesRe, "/")));
19532 //<debug>
19533 if (isNaN(date)) {
19534 Ext.Logger.error("Cannot parse the passed value " + value + " into a valid date");
19535 }
19536 //</debug>
19537 }
19538 value = date;
19539 }
19540 return Ext.Date.format(value, format || Ext.util.Format.defaultDateFormat);
19541 }
19542 });
19543
19544 /**
19545 * Represents an HTML fragment template. Templates may be {@link #compile precompiled} for greater performance.
19546 *
19547 * An instance of this class may be created by passing to the constructor either a single argument, or multiple
19548 * arguments. See the docs for {@link #constructor} for details.
19549 *
19550 * # Usage example
19551 *
19552 * var t = new Ext.Template(
19553 * '<div name="{id}">',
19554 * '<span class="{cls}">{name:trim} {value:ellipsis(10)}</span>',
19555 * '</div>',
19556 * // a configuration object:
19557 * {
19558 * compiled: true // compile immediately
19559 * }
19560 * );
19561 * t.compile();
19562 * t.append('some-element', {id: 'myid', cls: 'myclass', name: 'foo', value: 'bar'});
19563 *
19564 * # Notes
19565 *
19566 * - For a list of available format functions, see {@link Ext.util.Format}.
19567 * - `disableFormats` reduces `{@link #apply}` time when no formatting is required.
19568 */
19569 Ext.define('Ext.Template', {
19570
19571 /* Begin Definitions */
19572
19573
19574
19575 inheritableStatics: {
19576 /**
19577 * Creates a template from the passed element's value (_display:none_ textarea, preferred) or `innerHTML`.
19578 * @param {String/HTMLElement} el A DOM element or its `id`.
19579 * @param {Object} config (optional) Config object.
19580 * @return {Ext.Template} The created template.
19581 * @static
19582 * @inheritable
19583 */
19584 from: function(el, config) {
19585 el = Ext.getDom(el);
19586 return new this(el.value || el.innerHTML, config || '');
19587 }
19588 },
19589
19590 /* End Definitions */
19591
19592 /**
19593 * Creates new template.
19594 *
19595 * @param {Mixed[]/Mixed...} html List of strings to be concatenated into template and an
19596 * optional config object. One can either pass multiple arguments:
19597 *
19598 * new Ext.Template(
19599 * '<div name="{id}">',
19600 * '<span class="{cls}">{name} {value}</span>',
19601 * '</div>',
19602 * { compiled: true }
19603 * );
19604 *
19605 * or an array of these same things:
19606 *
19607 * new Ext.Template([
19608 * '<div name="{id}">',
19609 * '<span class="{cls}">{name} {value}</span>',
19610 * '</div>',
19611 * { compiled: true }
19612 * ]);
19613 *
19614 * Just a single string will also do for a simple template:
19615 *
19616 * new Ext.Template('<div name="{id}">{name}</div>');
19617 *
19618 */
19619 constructor: function(html) {
19620 var me = this,
19621 args = arguments,
19622 buffer = [],
19623 i = 0,
19624 length = args.length,
19625 value;
19626
19627 me.initialConfig = {};
19628
19629 // Allow an array to be passed here so we can
19630 // pass an array of strings and an object
19631 // at the end
19632 if (length === 1 && Ext.isArray(html)) {
19633 args = html;
19634 length = args.length;
19635 }
19636
19637 if (length > 1) {
19638 for (; i < length; i++) {
19639 value = args[i];
19640 if (typeof value == 'object') {
19641 Ext.apply(me.initialConfig, value);
19642 Ext.apply(me, value);
19643 } else {
19644 buffer.push(value);
19645 }
19646 }
19647 } else {
19648 buffer.push(html);
19649 }
19650
19651 // @private
19652 me.html = buffer.join('');
19653
19654 if (me.compiled) {
19655 me.compile();
19656 }
19657 },
19658
19659 /**
19660 * @property {Boolean} isTemplate
19661 * `true` in this class to identify an object as an instantiated Template, or subclass thereof.
19662 */
19663 isTemplate: true,
19664
19665 /**
19666 * @cfg {Boolean} [compiled=false]
19667 * `true` to immediately compile the template.
19668 */
19669
19670 /**
19671 * @cfg {Boolean} [disableFormats=false]
19672 * `true` to disable format functions in the template. If the template doesn't contain
19673 * format functions, setting `disableFormats` to `true` will reduce apply time.
19674 */
19675 disableFormats: false,
19676
19677 re: /\{([\w\-]+)(?:\:([\w\.]*)(?:\((.*?)?\))?)?\}/g,
19678
19679 /**
19680 * Returns an HTML fragment of this template with the specified values applied.
19681 *
19682 * @param {Object/Array} values The template values. Can be an array if your params are numeric:
19683 *
19684 * var tpl = new Ext.Template('Name: {0}, Age: {1}');
19685 * tpl.apply(['John', 25]);
19686 *
19687 * or an object:
19688 *
19689 * var tpl = new Ext.Template('Name: {name}, Age: {age}');
19690 * tpl.apply({name: 'John', age: 25});
19691 *
19692 * @return {String} The HTML fragment.
19693 */
19694 apply: function(values) {
19695 var me = this,
19696 useFormat = me.disableFormats !== true,
19697 fm = Ext.util.Format,
19698 tpl = me,
19699 ret;
19700
19701 if (me.compiled) {
19702 return me.compiled(values).join('');
19703 }
19704
19705 function fn(m, name, format, args) {
19706 if (format && useFormat) {
19707 if (args) {
19708 args = [values[name]].concat(Ext.functionFactory('return ['+ args +'];')());
19709 } else {
19710 args = [values[name]];
19711 }
19712 if (format.substr(0, 5) == "this.") {
19713 return tpl[format.substr(5)].apply(tpl, args);
19714 }
19715 else {
19716 return fm[format].apply(fm, args);
19717 }
19718 }
19719 else {
19720 return values[name] !== undefined ? values[name] : "";
19721 }
19722 }
19723
19724 ret = me.html.replace(me.re, fn);
19725 return ret;
19726 },
19727
19728 /**
19729 * Appends the result of this template to the provided output array.
19730 * @param {Object/Array} values The template values. See {@link #apply}.
19731 * @param {Array} out The array to which output is pushed.
19732 * @return {Array} The given out array.
19733 */
19734 applyOut: function(values, out) {
19735 var me = this;
19736
19737 if (me.compiled) {
19738 out.push.apply(out, me.compiled(values));
19739 } else {
19740 out.push(me.apply(values));
19741 }
19742
19743 return out;
19744 },
19745
19746 /**
19747 * @method applyTemplate
19748 * @member Ext.Template
19749 * Alias for {@link #apply}.
19750 * @inheritdoc Ext.Template#apply
19751 */
19752 applyTemplate: function () {
19753 return this.apply.apply(this, arguments);
19754 },
19755
19756 /**
19757 * Sets the HTML used as the template and optionally compiles it.
19758 * @param {String} html
19759 * @param {Boolean} compile (optional) `true` to compile the template.
19760 * @return {Ext.Template} this
19761 */
19762 set: function(html, compile) {
19763 var me = this;
19764 me.html = html;
19765 me.compiled = null;
19766 return compile ? me.compile() : me;
19767 },
19768
19769 compileARe: /\\/g,
19770 compileBRe: /(\r\n|\n)/g,
19771 compileCRe: /'/g,
19772
19773 /**
19774 * Compiles the template into an internal function, eliminating the RegEx overhead.
19775 * @return {Ext.Template} this
19776 */
19777 compile: function() {
19778 var me = this,
19779 fm = Ext.util.Format,
19780 useFormat = me.disableFormats !== true,
19781 body, bodyReturn;
19782
19783 function fn(m, name, format, args) {
19784 if (format && useFormat) {
19785 args = args ? ',' + args: "";
19786 if (format.substr(0, 5) != "this.") {
19787 format = "fm." + format + '(';
19788 }
19789 else {
19790 format = 'this.' + format.substr(5) + '(';
19791 }
19792 }
19793 else {
19794 args = '';
19795 format = "(values['" + name + "'] == undefined ? '' : ";
19796 }
19797 return "'," + format + "values['" + name + "']" + args + ") ,'";
19798 }
19799
19800 bodyReturn = me.html.replace(me.compileARe, '\\\\').replace(me.compileBRe, '\\n').replace(me.compileCRe, "\\'").replace(me.re, fn);
19801 body = "this.compiled = function(values){ return ['" + bodyReturn + "'];};";
19802 eval(body);
19803 return me;
19804 },
19805
19806 /**
19807 * Applies the supplied values to the template and inserts the new node(s) as the first child of el.
19808 *
19809 * @param {String/HTMLElement/Ext.Element} el The context element.
19810 * @param {Object/Array} values The template values. See {@link #applyTemplate} for details.
19811 * @param {Boolean} returnElement (optional) `true` to return a Ext.Element.
19812 * @return {HTMLElement/Ext.Element} The new node or Element.
19813 */
19814 insertFirst: function(el, values, returnElement) {
19815 return this.doInsert('afterBegin', el, values, returnElement);
19816 },
19817
19818 /**
19819 * Applies the supplied values to the template and inserts the new node(s) before el.
19820 *
19821 * @param {String/HTMLElement/Ext.Element} el The context element.
19822 * @param {Object/Array} values The template values. See {@link #applyTemplate} for details.
19823 * @param {Boolean} returnElement (optional) `true` to return an Ext.Element.
19824 * @return {HTMLElement/Ext.Element} The new node or Element
19825 */
19826 insertBefore: function(el, values, returnElement) {
19827 return this.doInsert('beforeBegin', el, values, returnElement);
19828 },
19829
19830 /**
19831 * Applies the supplied values to the template and inserts the new node(s) after el.
19832 *
19833 * @param {String/HTMLElement/Ext.Element} el The context element.
19834 * @param {Object/Array} values The template values. See {@link #applyTemplate} for details.
19835 * @param {Boolean} returnElement (optional) `true` to return a Ext.Element.
19836 * @return {HTMLElement/Ext.Element} The new node or Element.
19837 */
19838 insertAfter: function(el, values, returnElement) {
19839 return this.doInsert('afterEnd', el, values, returnElement);
19840 },
19841
19842 /**
19843 * Applies the supplied `values` to the template and appends the new node(s) to the specified `el`.
19844 *
19845 * For example usage see {@link Ext.Template Ext.Template class docs}.
19846 *
19847 * @param {String/HTMLElement/Ext.Element} el The context element.
19848 * @param {Object/Array} values The template values. See {@link #applyTemplate} for details.
19849 * @param {Boolean} returnElement (optional) true to return an Ext.Element.
19850 * @return {HTMLElement/Ext.Element} The new node or Element.
19851 */
19852 append: function(el, values, returnElement) {
19853 return this.doInsert('beforeEnd', el, values, returnElement);
19854 },
19855
19856 doInsert: function(where, el, values, returnElement) {
19857 var newNode = Ext.DomHelper.insertHtml(where, Ext.getDom(el), this.apply(values));
19858 return returnElement ? Ext.get(newNode) : newNode;
19859 },
19860
19861 /**
19862 * Applies the supplied values to the template and overwrites the content of el with the new node(s).
19863 *
19864 * @param {String/HTMLElement/Ext.Element} el The context element.
19865 * @param {Object/Array} values The template values. See {@link #applyTemplate} for details.
19866 * @param {Boolean} returnElement (optional) true to return a Ext.Element.
19867 * @return {HTMLElement/Ext.Element} The new node or Element.
19868 */
19869 overwrite: function(el, values, returnElement) {
19870 var newNode = Ext.DomHelper.overwrite(Ext.getDom(el), this.apply(values));
19871 return returnElement ? Ext.get(newNode) : newNode;
19872 }
19873 });
19874
19875 /**
19876 * This class parses the XTemplate syntax and calls abstract methods to process the parts.
19877 * @private
19878 */
19879 Ext.define('Ext.XTemplateParser', {
19880 constructor: function (config) {
19881 Ext.apply(this, config);
19882 },
19883
19884 /**
19885 * @property {Number} level The 'for' loop context level. This is adjusted up by one
19886 * prior to calling {@link #doFor} and down by one after calling the corresponding
19887 * {@link #doEnd} that closes the loop. This will be 1 on the first {@link #doFor}
19888 * call.
19889 */
19890
19891 /**
19892 * This method is called to process a piece of raw text from the tpl.
19893 * @param {String} text
19894 * @method doText
19895 */
19896 // doText: function (text)
19897
19898 /**
19899 * This method is called to process expressions (like `{[expr]}`).
19900 * @param {String} expr The body of the expression (inside "{[" and "]}").
19901 * @method doExpr
19902 */
19903 // doExpr: function (expr)
19904
19905 /**
19906 * This method is called to process simple tags (like `{tag}`).
19907 * @param {String} tag
19908 * @method doTag
19909 */
19910 // doTag: function (tag)
19911
19912 /**
19913 * This method is called to process `<tpl else>`.
19914 * @method doElse
19915 */
19916 // doElse: function ()
19917
19918 /**
19919 * This method is called to process `{% text %}`.
19920 * @param {String} text
19921 * @method doEval
19922 */
19923 // doEval: function (text)
19924
19925 /**
19926 * This method is called to process `<tpl if="action">`. If there are other attributes,
19927 * these are passed in the actions object.
19928 * @param {String} action
19929 * @param {Object} actions Other actions keyed by the attribute name (such as 'exec').
19930 * @method doIf
19931 */
19932 // doIf: function (action, actions)
19933
19934 /**
19935 * This method is called to process `<tpl elseif="action">`. If there are other attributes,
19936 * these are passed in the actions object.
19937 * @param {String} action
19938 * @param {Object} actions Other actions keyed by the attribute name (such as 'exec').
19939 * @method doElseIf
19940 */
19941 // doElseIf: function (action, actions)
19942
19943 /**
19944 * This method is called to process `<tpl switch="action">`. If there are other attributes,
19945 * these are passed in the actions object.
19946 * @param {String} action
19947 * @param {Object} actions Other actions keyed by the attribute name (such as 'exec').
19948 * @method doSwitch
19949 */
19950 // doSwitch: function (action, actions)
19951
19952 /**
19953 * This method is called to process `<tpl case="action">`. If there are other attributes,
19954 * these are passed in the actions object.
19955 * @param {String} action
19956 * @param {Object} actions Other actions keyed by the attribute name (such as 'exec').
19957 * @method doCase
19958 */
19959 // doCase: function (action, actions)
19960
19961 /**
19962 * This method is called to process `<tpl default>`.
19963 * @method doDefault
19964 */
19965 // doDefault: function ()
19966
19967 /**
19968 * This method is called to process `</tpl>`. It is given the action type that started
19969 * the tpl and the set of additional actions.
19970 * @param {String} type The type of action that is being ended.
19971 * @param {Object} actions The other actions keyed by the attribute name (such as 'exec').
19972 * @method doEnd
19973 */
19974 // doEnd: function (type, actions)
19975
19976 /**
19977 * This method is called to process `<tpl for="action">`. If there are other attributes,
19978 * these are passed in the actions object.
19979 * @param {String} action
19980 * @param {Object} actions Other actions keyed by the attribute name (such as 'exec').
19981 * @method doFor
19982 */
19983 // doFor: function (action, actions)
19984
19985 /**
19986 * This method is called to process `<tpl exec="action">`. If there are other attributes,
19987 * these are passed in the actions object.
19988 * @param {String} action
19989 * @param {Object} actions Other actions keyed by the attribute name.
19990 * @method doExec
19991 */
19992 // doExec: function (action, actions)
19993
19994 /**
19995 * This method is called to process an empty `<tpl>`. This is unlikely to need to be
19996 * implemented, so a default (do nothing) version is provided.
19997 * @method
19998 */
19999 doTpl: Ext.emptyFn,
20000
20001 parse: function (str) {
20002 var me = this,
20003 len = str.length,
20004 aliases = { elseif: 'elif' },
20005 topRe = me.topRe,
20006 actionsRe = me.actionsRe,
20007 index, stack, s, m, t, prev, frame, subMatch, begin, end, actions,
20008 prop;
20009
20010 me.level = 0;
20011 me.stack = stack = [];
20012
20013 for (index = 0; index < len; index = end) {
20014 topRe.lastIndex = index;
20015 m = topRe.exec(str);
20016
20017 if (!m) {
20018 me.doText(str.substring(index, len));
20019 break;
20020 }
20021
20022 begin = m.index;
20023 end = topRe.lastIndex;
20024
20025 if (index < begin) {
20026 me.doText(str.substring(index, begin));
20027 }
20028
20029 if (m[1]) {
20030 end = str.indexOf('%}', begin+2);
20031 me.doEval(str.substring(begin+2, end));
20032 end += 2;
20033 } else if (m[2]) {
20034 end = str.indexOf(']}', begin+2);
20035 me.doExpr(str.substring(begin+2, end));
20036 end += 2;
20037 } else if (m[3]) { // if ('{' token)
20038 me.doTag(m[3]);
20039 } else if (m[4]) { // content of a <tpl xxxxxx xxx> tag
20040 actions = null;
20041 while ((subMatch = actionsRe.exec(m[4])) !== null) {
20042 s = subMatch[2] || subMatch[3];
20043 if (s) {
20044 s = Ext.String.htmlDecode(s); // decode attr value
20045 t = subMatch[1];
20046 t = aliases[t] || t;
20047 actions = actions || {};
20048 prev = actions[t];
20049
20050 if (typeof prev == 'string') {
20051 actions[t] = [prev, s];
20052 } else if (prev) {
20053 actions[t].push(s);
20054 } else {
20055 actions[t] = s;
20056 }
20057 }
20058 }
20059
20060 if (!actions) {
20061 if (me.elseRe.test(m[4])) {
20062 me.doElse();
20063 } else if (me.defaultRe.test(m[4])) {
20064 me.doDefault();
20065 } else {
20066 me.doTpl();
20067 stack.push({ type: 'tpl' });
20068 }
20069 }
20070 else if (actions['if']) {
20071 me.doIf(actions['if'], actions);
20072 stack.push({ type: 'if' });
20073 }
20074 else if (actions['switch']) {
20075 me.doSwitch(actions['switch'], actions);
20076 stack.push({ type: 'switch' });
20077 }
20078 else if (actions['case']) {
20079 me.doCase(actions['case'], actions);
20080 }
20081 else if (actions['elif']) {
20082 me.doElseIf(actions['elif'], actions);
20083 }
20084 else if (actions['for']) {
20085 ++me.level;
20086
20087 // Extract property name to use from indexed item
20088 if (prop = me.propRe.exec(m[4])) {
20089 actions.propName = prop[1] || prop[2];
20090 }
20091 me.doFor(actions['for'], actions);
20092 stack.push({ type: 'for', actions: actions });
20093 }
20094 else if (actions.exec) {
20095 me.doExec(actions.exec, actions);
20096 stack.push({ type: 'exec', actions: actions });
20097 }
20098 /*
20099 else {
20100 // todo - error
20101 }
20102 */
20103 } else if (m[0].length === 5) {
20104 // if the length of m[0] is 5, assume that we're dealing with an opening tpl tag with no attributes (e.g. <tpl>...</tpl>)
20105 // in this case no action is needed other than pushing it on to the stack
20106 stack.push({ type: 'tpl' });
20107 } else {
20108 frame = stack.pop();
20109 me.doEnd(frame.type, frame.actions);
20110 if (frame.type == 'for') {
20111 --me.level;
20112 }
20113 }
20114 }
20115 },
20116
20117 // Internal regexes
20118
20119 topRe: /(?:(\{\%)|(\{\[)|\{([^{}]*)\})|(?:<tpl([^>]*)\>)|(?:<\/tpl>)/g,
20120 actionsRe: /\s*(elif|elseif|if|for|exec|switch|case|eval)\s*\=\s*(?:(?:"([^"]*)")|(?:'([^']*)'))\s*/g,
20121 propRe: /prop=(?:(?:"([^"]*)")|(?:'([^']*)'))/,
20122 defaultRe: /^\s*default\s*$/,
20123 elseRe: /^\s*else\s*$/
20124 });
20125
20126 /**
20127 * This class compiles the XTemplate syntax into a function object. The function is used
20128 * like so:
20129 *
20130 * function (out, values, parent, xindex, xcount) {
20131 * // out is the output array to store results
20132 * // values, parent, xindex and xcount have their historical meaning
20133 * }
20134 *
20135 * @private
20136 */
20137 Ext.define('Ext.XTemplateCompiler', {
20138 extend: Ext.XTemplateParser ,
20139
20140 // Chrome really likes "new Function" to realize the code block (as in it is
20141 // 2x-3x faster to call it than using eval), but Firefox chokes on it badly.
20142 // IE and Opera are also fine with the "new Function" technique.
20143 useEval: Ext.isGecko,
20144
20145 // See [http://jsperf.com/nige-array-append](http://jsperf.com/nige-array-append) for quickest way to append to an array of unknown length
20146 // (Due to arbitrary code execution inside a template, we cannot easily track the length in var)
20147 // On IE6 and 7 `myArray[myArray.length]='foo'` is better. On other browsers `myArray.push('foo')` is better.
20148 useIndex: Ext.isIE6 || Ext.isIE7,
20149
20150 useFormat: true,
20151
20152 propNameRe: /^[\w\d\$]*$/,
20153
20154 compile: function (tpl) {
20155 var me = this,
20156 code = me.generate(tpl);
20157
20158 // When using "new Function", we have to pass our "Ext" variable to it in order to
20159 // support sandboxing. If we did not, the generated function would use the global
20160 // "Ext", not the "Ext" from our sandbox (scope chain).
20161 //
20162 return me.useEval ? me.evalTpl(code) : (new Function('Ext', code))(Ext);
20163 },
20164
20165 generate: function (tpl) {
20166 var me = this,
20167 // note: Ext here is properly sandboxed
20168 definitions = 'var fm=Ext.util.Format,ts=Object.prototype.toString;',
20169 code;
20170
20171 // Track how many levels we use, so that we only "var" each level's variables once
20172 me.maxLevel = 0;
20173
20174 me.body = [
20175 'var c0=values, a0=' + me.createArrayTest(0) + ', p0=parent, n0=xcount, i0=xindex, v;\n'
20176 ];
20177 if (me.definitions) {
20178 if (typeof me.definitions === 'string') {
20179 me.definitions = [me.definitions, definitions ];
20180 } else {
20181 me.definitions.push(definitions);
20182 }
20183 } else {
20184 me.definitions = [ definitions ];
20185 }
20186 me.switches = [];
20187
20188 me.parse(tpl);
20189
20190 me.definitions.push(
20191 (me.useEval ? '$=' : 'return') + ' function (' + me.fnArgs + ') {',
20192 me.body.join(''),
20193 '}'
20194 );
20195
20196 code = me.definitions.join('\n');
20197
20198 // Free up the arrays.
20199 me.definitions.length = me.body.length = me.switches.length = 0;
20200 delete me.definitions;
20201 delete me.body;
20202 delete me.switches;
20203
20204 return code;
20205 },
20206
20207 //-----------------------------------
20208 // XTemplateParser callouts
20209
20210 //
20211 doText: function (text) {
20212 var me = this,
20213 out = me.body;
20214
20215 text = text.replace(me.aposRe, "\\'").replace(me.newLineRe, '\\n');
20216 if (me.useIndex) {
20217 out.push('out[out.length]=\'', text, '\'\n');
20218 } else {
20219 out.push('out.push(\'', text, '\')\n');
20220 }
20221 },
20222
20223 doExpr: function (expr) {
20224 var out = this.body;
20225 out.push('v=' + expr + '; if (v !== undefined && v !== null) out');
20226
20227 // Coerce value to string using concatenation of an empty string literal.
20228 // See http://jsperf.com/tostringvscoercion/5
20229 if (this.useIndex) {
20230 out.push('[out.length]=v+\'\'\n');
20231 } else {
20232 out.push('.push(v+\'\')\n');
20233 }
20234 },
20235
20236 doTag: function (tag) {
20237 this.doExpr(this.parseTag(tag));
20238 },
20239
20240 doElse: function () {
20241 this.body.push('} else {\n');
20242 },
20243
20244 doEval: function (text) {
20245 this.body.push(text, '\n');
20246 },
20247
20248 doIf: function (action, actions) {
20249 var me = this;
20250
20251 // If it's just a propName, use it directly in the if
20252 if (action === '.') {
20253 me.body.push('if (values) {\n');
20254 } else if (me.propNameRe.test(action)) {
20255 me.body.push('if (', me.parseTag(action), ') {\n');
20256 }
20257 // Otherwise, it must be an expression, and needs to be returned from an fn which uses with(values)
20258 else {
20259 me.body.push('if (', me.addFn(action), me.callFn, ') {\n');
20260 }
20261 if (actions.exec) {
20262 me.doExec(actions.exec);
20263 }
20264 },
20265
20266 doElseIf: function (action, actions) {
20267 var me = this;
20268
20269 // If it's just a propName, use it directly in the else if
20270 if (action === '.') {
20271 me.body.push('else if (values) {\n');
20272 } else if (me.propNameRe.test(action)) {
20273 me.body.push('} else if (', me.parseTag(action), ') {\n');
20274 }
20275 // Otherwise, it must be an expression, and needs to be returned from an fn which uses with(values)
20276 else {
20277 me.body.push('} else if (', me.addFn(action), me.callFn, ') {\n');
20278 }
20279 if (actions.exec) {
20280 me.doExec(actions.exec);
20281 }
20282 },
20283
20284 doSwitch: function (action) {
20285 var me = this;
20286
20287 // If it's just a propName, use it directly in the switch
20288 if (action === '.') {
20289 me.body.push('switch (values) {\n');
20290 } else if (me.propNameRe.test(action)) {
20291 me.body.push('switch (', me.parseTag(action), ') {\n');
20292 }
20293 // Otherwise, it must be an expression, and needs to be returned from an fn which uses with(values)
20294 else {
20295 me.body.push('switch (', me.addFn(action), me.callFn, ') {\n');
20296 }
20297 me.switches.push(0);
20298 },
20299
20300 doCase: function (action) {
20301 var me = this,
20302 cases = Ext.isArray(action) ? action : [action],
20303 n = me.switches.length - 1,
20304 match, i;
20305
20306 if (me.switches[n]) {
20307 me.body.push('break;\n');
20308 } else {
20309 me.switches[n]++;
20310 }
20311
20312 for (i = 0, n = cases.length; i < n; ++i) {
20313 match = me.intRe.exec(cases[i]);
20314 cases[i] = match ? match[1] : ("'" + cases[i].replace(me.aposRe,"\\'") + "'");
20315 }
20316
20317 me.body.push('case ', cases.join(': case '), ':\n');
20318 },
20319
20320 doDefault: function () {
20321 var me = this,
20322 n = me.switches.length - 1;
20323
20324 if (me.switches[n]) {
20325 me.body.push('break;\n');
20326 } else {
20327 me.switches[n]++;
20328 }
20329
20330 me.body.push('default:\n');
20331 },
20332
20333 doEnd: function (type, actions) {
20334 var me = this,
20335 L = me.level-1;
20336
20337 if (type == 'for') {
20338 /*
20339 To exit a for loop we must restore the outer loop's context. The code looks
20340 like this (which goes with that produced by doFor:
20341
20342 for (...) { // the part generated by doFor
20343 ... // the body of the for loop
20344
20345 // ... any tpl for exec statement goes here...
20346 }
20347 parent = p1;
20348 values = r2;
20349 xcount = n1;
20350 xindex = i1
20351 */
20352 if (actions.exec) {
20353 me.doExec(actions.exec);
20354 }
20355
20356 me.body.push('}\n');
20357 me.body.push('parent=p',L,';values=r',L+1,';xcount=n',L,';xindex=i',L,'\n');
20358 } else if (type == 'if' || type == 'switch') {
20359 me.body.push('}\n');
20360 }
20361 },
20362
20363 doFor: function (action, actions) {
20364 var me = this,
20365 s,
20366 L = me.level,
20367 up = L-1,
20368 pL = 'p' + L,
20369 parentAssignment;
20370
20371 // If it's just a propName, use it directly in the switch
20372 if (action === '.') {
20373 s = 'values';
20374 } else if (me.propNameRe.test(action)) {
20375 s = me.parseTag(action);
20376 }
20377 // Otherwise, it must be an expression, and needs to be returned from an fn which uses with(values)
20378 else {
20379 s = me.addFn(action) + me.callFn;
20380 }
20381
20382 /*
20383 We are trying to produce a block of code that looks like below. We use the nesting
20384 level to uniquely name the control variables.
20385
20386 // Omit "var " if we have already been through level 2
20387 var i2 = 0,
20388 n2 = 0,
20389 c2 = values['propName'],
20390 // c2 is the context object for the for loop
20391 a2 = Array.isArray(c2);
20392 p2 = c1,
20393 // p2 is the parent context (of the outer for loop)
20394 r2 = values
20395 // r2 is the values object to
20396
20397 // If iterating over the current data, the parent is always set to c2
20398 parent = c2;
20399 // If iterating over a property in an object, set the parent to the object
20400 parent = a1 ? c1[i1] : p2 // set parent
20401 if (c2) {
20402 if (a2) {
20403 n2 = c2.length;
20404 } else if (c2.isMixedCollection) {
20405 c2 = c2.items;
20406 n2 = c2.length;
20407 } else if (c2.isStore) {
20408 c2 = c2.data.items;
20409 n2 = c2.length;
20410 } else {
20411 c2 = [ c2 ];
20412 n2 = 1;
20413 }
20414 }
20415 // i2 is the loop index and n2 is the number (xcount) of this for loop
20416 for (xcount = n2; i2 < n2; ++i2) {
20417 values = c2[i2] // adjust special vars to inner scope
20418 xindex = i2 + 1 // xindex is 1-based
20419
20420 The body of the loop is whatever comes between the tpl and /tpl statements (which
20421 is handled by doEnd).
20422 */
20423
20424 // Declare the vars for a particular level only if we have not already declared them.
20425 if (me.maxLevel < L) {
20426 me.maxLevel = L;
20427 me.body.push('var ');
20428 }
20429
20430 if (action == '.') {
20431 parentAssignment = 'c' + L;
20432 } else {
20433 parentAssignment = 'a' + up + '?c' + up + '[i' + up + ']:p' + L;
20434 }
20435
20436 me.body.push('i',L,'=0,n', L, '=0,c',L,'=',s,',a',L,'=', me.createArrayTest(L), ',p',L,'=c',up,',r',L,'=values;\n',
20437 'parent=',parentAssignment,'\n',
20438 'if (c',L,'){if(a',L,'){n', L,'=c', L, '.length;}else if (c', L, '.isMixedCollection){c',L,'=c',L,'.items;n',L,'=c',L,'.length;}else if(c',L,'.isStore){c',L,'=c',L,'.data.items;n',L,'=c',L,'.length;}else{c',L,'=[c',L,'];n',L,'=1;}}\n',
20439 'for (xcount=n',L,';i',L,'<n'+L+';++i',L,'){\n',
20440 'values=c',L,'[i',L,']');
20441 if (actions.propName) {
20442 me.body.push('.', actions.propName);
20443 }
20444 me.body.push('\n',
20445 'xindex=i',L,'+1\n');
20446 },
20447
20448 createArrayTest: ('isArray' in Array) ? function(L) {
20449 return 'Array.isArray(c' + L + ')';
20450 } : function(L) {
20451 return 'ts.call(c' + L + ')==="[object Array]"';
20452 },
20453
20454 doExec: function (action, actions) {
20455 var me = this,
20456 name = 'f' + me.definitions.length;
20457
20458 me.definitions.push('function ' + name + '(' + me.fnArgs + ') {',
20459 ' try { with(values) {',
20460 ' ' + action,
20461 ' }} catch(e) {',
20462 //<debug>
20463 'Ext.Logger.log("XTemplate Error: " + e.message);',
20464 //</debug>
20465 '}',
20466 '}');
20467
20468 me.body.push(name + me.callFn + '\n');
20469 },
20470
20471 //-----------------------------------
20472 // Internal
20473
20474 //
20475 addFn: function (body) {
20476 var me = this,
20477 name = 'f' + me.definitions.length;
20478
20479 if (body === '.') {
20480 me.definitions.push('function ' + name + '(' + me.fnArgs + ') {',
20481 ' return values',
20482 '}');
20483 } else if (body === '..') {
20484 me.definitions.push('function ' + name + '(' + me.fnArgs + ') {',
20485 ' return parent',
20486 '}');
20487 } else {
20488 me.definitions.push('function ' + name + '(' + me.fnArgs + ') {',
20489 ' try { with(values) {',
20490 ' return(' + body + ')',
20491 ' }} catch(e) {',
20492 //<debug>
20493 'Ext.Logger.log("XTemplate Error: " + e.message);',
20494 //</debug>
20495 '}',
20496 '}');
20497 }
20498
20499 return name;
20500 },
20501
20502 parseTag: function (tag) {
20503 var me = this,
20504 m = me.tagRe.exec(tag),
20505 name = m[1],
20506 format = m[2],
20507 args = m[3],
20508 math = m[4],
20509 v;
20510
20511 // name = "." - Just use the values object.
20512 if (name == '.') {
20513 // filter to not include arrays/objects/nulls
20514 if (!me.validTypes) {
20515 me.definitions.push('var validTypes={string:1,number:1,boolean:1};');
20516 me.validTypes = true;
20517 }
20518 v = 'validTypes[typeof values] || ts.call(values) === "[object Date]" ? values : ""';
20519 }
20520 // name = "#" - Use the xindex
20521 else if (name == '#') {
20522 v = 'xindex';
20523 }
20524 else if (name.substr(0, 7) == "parent.") {
20525 v = name;
20526 }
20527 // compound JavaScript property name (e.g., "foo.bar")
20528 else if (isNaN(name) && name.indexOf('-') == -1 && name.indexOf('.') != -1) {
20529 v = "values." + name;
20530 }
20531 // number or a '-' in it or a single word (maybe a keyword): use array notation
20532 // (http://jsperf.com/string-property-access/4)
20533 else {
20534 v = "values['" + name + "']";
20535 }
20536
20537 if (math) {
20538 v = '(' + v + math + ')';
20539 }
20540
20541 if (format && me.useFormat) {
20542 args = args ? ',' + args : "";
20543 if (format.substr(0, 5) != "this.") {
20544 format = "fm." + format + '(';
20545 } else {
20546 format += '(';
20547 }
20548 } else {
20549 return v;
20550 }
20551
20552 return format + v + args + ')';
20553 },
20554
20555 // @private
20556 evalTpl: function ($) {
20557
20558 // We have to use eval to realize the code block and capture the inner func we also
20559 // don't want a deep scope chain. We only do this in Firefox and it is also unhappy
20560 // with eval containing a return statement, so instead we assign to "$" and return
20561 // that. Because we use "eval", we are automatically sandboxed properly.
20562 eval($);
20563 return $;
20564 },
20565
20566 newLineRe: /\r\n|\r|\n/g,
20567 aposRe: /[']/g,
20568 intRe: /^\s*(\d+)\s*$/,
20569 tagRe: /([\w-\.\#\$]+)(?:\:([\w\.]*)(?:\((.*?)?\))?)?(\s?[\+\-\*\/]\s?[\d\.\+\-\*\/\(\)]+)?/
20570 }, function () {
20571 var proto = this.prototype;
20572
20573 proto.fnArgs = 'out,values,parent,xindex,xcount';
20574 proto.callFn = '.call(this,' + proto.fnArgs + ')';
20575 });
20576
20577 /**
20578 * A template class that supports advanced functionality like:
20579 *
20580 * - Autofilling arrays using templates and sub-templates
20581 * - Conditional processing with basic comparison operators
20582 * - Basic math function support
20583 * - Execute arbitrary inline code with special built-in template variables
20584 * - Custom member functions
20585 * - 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
20586 *
20587 * XTemplate provides the templating mechanism built into {@link Ext.DataView}.
20588 *
20589 * The {@link Ext.Template} describes the acceptable parameters to pass to the constructor. The following examples
20590 * demonstrate all of the supported features.
20591 *
20592 * # Sample Data
20593 *
20594 * This is the data object used for reference in each code example:
20595 *
20596 * var data = {
20597 * name: 'Don Griffin',
20598 * title: 'Senior Technomage',
20599 * company: 'Sencha Inc.',
20600 * drinks: ['Coffee', 'Water', 'More Coffee'],
20601 * kids: [
20602 * { name: 'Aubrey', age: 17 },
20603 * { name: 'Joshua', age: 13 },
20604 * { name: 'Cale', age: 10 },
20605 * { name: 'Nikol', age: 5 },
20606 * { name: 'Solomon', age: 0 }
20607 * ]
20608 * };
20609 *
20610 * # Auto filling of arrays
20611 *
20612 * The **tpl** tag and the **for** operator are used to process the provided data object:
20613 *
20614 * - If the value specified in for is an array, it will auto-fill, repeating the template block inside the tpl
20615 * tag for each item in the array.
20616 * - If for="." is specified, the data object provided is examined.
20617 * - While processing an array, the special variable {#} will provide the current array index + 1 (starts at 1, not 0).
20618 *
20619 * Examples:
20620 *
20621 * <tpl for=".">...</tpl> // loop through array at root node
20622 * <tpl for="foo">...</tpl> // loop through array at foo node
20623 * <tpl for="foo.bar">...</tpl> // loop through array at foo.bar node
20624 *
20625 * Using the sample data above:
20626 *
20627 * var tpl = new Ext.XTemplate(
20628 * '<p>Kids: ',
20629 * '<tpl for=".">', // process the data.kids node
20630 * '<p>{#}. {name}</p>', // use current array index to autonumber
20631 * '</tpl></p>'
20632 * );
20633 * tpl.overwrite(panel.body, data.kids); // pass the kids property of the data object
20634 *
20635 * An example illustrating how the **for** property can be leveraged to access specified members of the provided data
20636 * object to populate the template:
20637 *
20638 * var tpl = new Ext.XTemplate(
20639 * '<p>Name: {name}</p>',
20640 * '<p>Title: {title}</p>',
20641 * '<p>Company: {company}</p>',
20642 * '<p>Kids: ',
20643 * '<tpl for="kids">', // interrogate the kids property within the data
20644 * '<p>{name}</p>',
20645 * '</tpl></p>'
20646 * );
20647 * tpl.overwrite(panel.body, data); // pass the root node of the data object
20648 *
20649 * Flat arrays that contain values (and not objects) can be auto-rendered using the special **`{.}`** variable inside a
20650 * loop. This variable will represent the value of the array at the current index:
20651 *
20652 * var tpl = new Ext.XTemplate(
20653 * '<p>{name}\'s favorite beverages:</p>',
20654 * '<tpl for="drinks">',
20655 * '<div> - {.}</div>',
20656 * '</tpl>'
20657 * );
20658 * tpl.overwrite(panel.body, data);
20659 *
20660 * When processing a sub-template, for example while looping through a child array, you can access the parent object's
20661 * members via the **parent** object:
20662 *
20663 * var tpl = new Ext.XTemplate(
20664 * '<p>Name: {name}</p>',
20665 * '<p>Kids: ',
20666 * '<tpl for="kids">',
20667 * '<tpl if="age &gt; 1">',
20668 * '<p>{name}</p>',
20669 * '<p>Dad: {parent.name}</p>',
20670 * '</tpl>',
20671 * '</tpl></p>'
20672 * );
20673 * tpl.overwrite(panel.body, data);
20674 *
20675 * # Conditional processing with basic comparison operators
20676 *
20677 * The **tpl** tag and the **if** operator are used to provide conditional checks for deciding whether or not to render
20678 * specific parts of the template.
20679 *
20680 * Using the sample data above:
20681 *
20682 * var tpl = new Ext.XTemplate(
20683 * '<p>Name: {name}</p>',
20684 * '<p>Kids: ',
20685 * '<tpl for="kids">',
20686 * '<tpl if="age &gt; 1">',
20687 * '<p>{name}</p>',
20688 * '</tpl>',
20689 * '</tpl></p>'
20690 * );
20691 * tpl.overwrite(panel.body, data);
20692 *
20693 * More advanced conditionals are also supported:
20694 *
20695 * var tpl = new Ext.XTemplate(
20696 * '<p>Name: {name}</p>',
20697 * '<p>Kids: ',
20698 * '<tpl for="kids">',
20699 * '<p>{name} is a ',
20700 * '<tpl if="age &gt;= 13">',
20701 * '<p>teenager</p>',
20702 * '<tpl elseif="age &gt;= 2">',
20703 * '<p>kid</p>',
20704 * '<tpl else>',
20705 * '<p>baby</p>',
20706 * '</tpl>',
20707 * '</tpl></p>'
20708 * );
20709 *
20710 * var tpl = new Ext.XTemplate(
20711 * '<p>Name: {name}</p>',
20712 * '<p>Kids: ',
20713 * '<tpl for="kids">',
20714 * '<p>{name} is a ',
20715 * '<tpl switch="name">',
20716 * '<tpl case="Aubrey" case="Nikol">',
20717 * '<p>girl</p>',
20718 * '<tpl default">',
20719 * '<p>boy</p>',
20720 * '</tpl>',
20721 * '</tpl></p>'
20722 * );
20723 *
20724 * A `break` is implied between each case and default, however, multiple cases can be listed
20725 * in a single &lt;tpl&gt; tag.
20726 *
20727 * # Using double quotes
20728 *
20729 * Examples:
20730 *
20731 * var tpl = new Ext.XTemplate(
20732 * "<tpl if='age &gt; 1 && age &lt; 10'>Child</tpl>",
20733 * "<tpl if='age &gt;= 10 && age &lt; 18'>Teenager</tpl>",
20734 * "<tpl if='this.isGirl(name)'>...</tpl>",
20735 * '<tpl if="id == \'download\'">...</tpl>',
20736 * "<tpl if='needsIcon'><img src='{icon}' class='{iconCls}'/></tpl>",
20737 * "<tpl if='name == \"Don\"'>Hello</tpl>"
20738 * );
20739 *
20740 * # Basic math support
20741 *
20742 * The following basic math operators may be applied directly on numeric data values:
20743 *
20744 * + - * /
20745 *
20746 * For example:
20747 *
20748 * var tpl = new Ext.XTemplate(
20749 * '<p>Name: {name}</p>',
20750 * '<p>Kids: ',
20751 * '<tpl for="kids">',
20752 * '<tpl if="age &gt; 1">', // <-- Note that the > is encoded
20753 * '<p>{#}: {name}</p>', // <-- Auto-number each item
20754 * '<p>In 5 Years: {age+5}</p>', // <-- Basic math
20755 * '<p>Dad: {parent.name}</p>',
20756 * '</tpl>',
20757 * '</tpl></p>'
20758 * );
20759 * tpl.overwrite(panel.body, data);
20760 *
20761 * # Execute arbitrary inline code with special built-in template variables
20762 *
20763 * Anything between `{[ ... ]}` is considered code to be executed in the scope of the template.
20764 * The expression is evaluated and the result is included in the generated result. There are
20765 * some special variables available in that code:
20766 *
20767 * - **out**: The output array into which the template is being appended (using `push` to later
20768 * `join`).
20769 * - **values**: The values in the current scope. If you are using scope changing sub-templates,
20770 * you can change what values is.
20771 * - **parent**: The scope (values) of the ancestor template.
20772 * - **xindex**: If you are in a looping template, the index of the loop you are in (1-based).
20773 * - **xcount**: If you are in a looping template, the total length of the array you are looping.
20774 *
20775 * This example demonstrates basic row striping using an inline code block and the xindex variable:
20776 *
20777 * var tpl = new Ext.XTemplate(
20778 * '<p>Name: {name}</p>',
20779 * '<p>Company: {[values.company.toUpperCase() + ", " + values.title]}</p>',
20780 * '<p>Kids: ',
20781 * '<tpl for="kids">',
20782 * '<div class="{[xindex % 2 === 0 ? "even" : "odd"]}">',
20783 * '{name}',
20784 * '</div>',
20785 * '</tpl></p>'
20786 * );
20787 *
20788 * Any code contained in "verbatim" blocks (using "{% ... %}") will be inserted directly in
20789 * the generated code for the template. These blocks are not included in the output. This
20790 * can be used for simple things like break/continue in a loop, or control structures or
20791 * method calls (when they don't produce output). The `this` references the template instance.
20792 *
20793 * var tpl = new Ext.XTemplate(
20794 * '<p>Name: {name}</p>',
20795 * '<p>Company: {[values.company.toUpperCase() + ", " + values.title]}</p>',
20796 * '<p>Kids: ',
20797 * '<tpl for="kids">',
20798 * '{% if (xindex % 2 === 0) continue; %}',
20799 * '{name}',
20800 * '{% if (xindex > 100) break; %}',
20801 * '</div>',
20802 * '</tpl></p>'
20803 * );
20804 *
20805 * # Template member functions
20806 *
20807 * One or more member functions can be specified in a configuration object passed into the XTemplate constructor for
20808 * more complex processing:
20809 *
20810 * var tpl = new Ext.XTemplate(
20811 * '<p>Name: {name}</p>',
20812 * '<p>Kids: ',
20813 * '<tpl for="kids">',
20814 * '<tpl if="this.isGirl(name)">',
20815 * '<p>Girl: {name} - {age}</p>',
20816 * '<tpl else>',
20817 * '<p>Boy: {name} - {age}</p>',
20818 * '</tpl>',
20819 * '<tpl if="this.isBaby(age)">',
20820 * '<p>{name} is a baby!</p>',
20821 * '</tpl>',
20822 * '</tpl></p>',
20823 * {
20824 * // XTemplate configuration:
20825 * disableFormats: true,
20826 * // member functions:
20827 * isGirl: function(name){
20828 * return name == 'Sara Grace';
20829 * },
20830 * isBaby: function(age){
20831 * return age < 1;
20832 * }
20833 * }
20834 * );
20835 * tpl.overwrite(panel.body, data);
20836 */
20837 Ext.define('Ext.XTemplate', {
20838 extend: Ext.Template ,
20839
20840
20841
20842 /**
20843 * @private
20844 */
20845 emptyObj: {},
20846
20847 /**
20848 * @cfg {Boolean} compiled
20849 * Only applies to {@link Ext.Template}, XTemplates are compiled automatically on the
20850 * first call to {@link #apply} or {@link #applyOut}.
20851 * @hide
20852 */
20853
20854 apply: function(values) {
20855 return this.applyOut(values, []).join('');
20856 },
20857
20858 /**
20859 * Appends the result of this template to the provided output array.
20860 * @param {Object/Array} values The template values. See {@link #apply}.
20861 * @param {Array} out The array to which output is pushed.
20862 * @param {Object} parent
20863 * @return {Array} The given out array.
20864 */
20865 applyOut: function(values, out, parent) {
20866 var me = this,
20867 xindex = values.xindex,
20868 xcount = values.xcount,
20869 compiler;
20870
20871 if (!me.fn) {
20872 compiler = new Ext.XTemplateCompiler({
20873 useFormat : me.disableFormats !== true,
20874 definitions : me.definitions
20875 });
20876
20877 me.fn = compiler.compile(me.html);
20878 }
20879
20880 try {
20881 xindex = typeof xindex === 'number' ? xindex : 1;
20882 xcount = typeof xcount === 'number' ? xcount : 1;
20883
20884 me.fn.call(me, out, values, parent || me.emptyObj, xindex, xcount);
20885 } catch (e) {
20886 //<debug>
20887 Ext.Logger.log('Error: ' + e.message);
20888 //</debug>
20889 }
20890
20891 return out;
20892 },
20893
20894 /**
20895 * Does nothing. XTemplates are compiled automatically, so this function simply returns this.
20896 * @return {Ext.XTemplate} this
20897 */
20898 compile: function() {
20899 return this;
20900 },
20901
20902 statics: {
20903 /**
20904 * Gets an `XTemplate` from an object (an instance of an {@link Ext#define}'d class).
20905 * Many times, templates are configured high in the class hierarchy and are to be
20906 * shared by all classes that derive from that base. To further complicate matters,
20907 * these templates are seldom actual instances but are rather configurations. For
20908 * example:
20909 *
20910 * Ext.define('MyApp.Class', {
20911 * someTpl: [
20912 * 'tpl text here'
20913 * ]
20914 * });
20915 *
20916 * The goal being to share that template definition with all instances and even
20917 * instances of derived classes, until `someTpl` is overridden. This method will
20918 * "upgrade" these configurations to be real `XTemplate` instances *in place* (to
20919 * avoid creating one instance per object).
20920 *
20921 * @param {Object} instance The object from which to get the `XTemplate` (must be
20922 * an instance of an {@link Ext#define}'d class).
20923 * @param {String} name The name of the property by which to get the `XTemplate`.
20924 * @return {Ext.XTemplate} The `XTemplate` instance or null if not found.
20925 * @protected
20926 */
20927 getTpl: function (instance, name) {
20928 var tpl = instance[name], // go for it! 99% of the time we will get it!
20929 proto;
20930
20931 if (tpl && !tpl.isTemplate) { // tpl is just a configuration (not an instance)
20932 // create the template instance from the configuration:
20933 tpl = Ext.ClassManager.dynInstantiate('Ext.XTemplate', tpl);
20934
20935 // and replace the reference with the new instance:
20936 if (instance.hasOwnProperty(name)) { // the tpl is on the instance
20937 instance[name] = tpl;
20938 } else { // must be somewhere in the prototype chain
20939 for (proto = instance.self.prototype; proto; proto = proto.superclass) {
20940 if (proto.hasOwnProperty(name)) {
20941 proto[name] = tpl;
20942 break;
20943 }
20944 }
20945 }
20946 }
20947 // else !tpl (no such tpl) or the tpl is an instance already... either way, tpl
20948 // is ready to return
20949
20950 return tpl || null;
20951 }
20952 }
20953 });
20954
20955 /**
20956 * @private
20957 */
20958 Ext.define('Ext.behavior.Behavior', {
20959 constructor: function(component) {
20960 this.component = component;
20961
20962 component.on('destroy', 'onComponentDestroy', this);
20963 },
20964
20965 onComponentDestroy: Ext.emptyFn
20966 });
20967
20968 /**
20969 * @private
20970 */
20971 Ext.define('Ext.fx.easing.Abstract', {
20972
20973 config: {
20974 startTime: 0,
20975 startValue: 0
20976 },
20977
20978 isEasing: true,
20979
20980 isEnded: false,
20981
20982 constructor: function(config) {
20983 this.initConfig(config);
20984
20985 return this;
20986 },
20987
20988 applyStartTime: function(startTime) {
20989 if (!startTime) {
20990 startTime = Ext.Date.now();
20991 }
20992
20993 return startTime;
20994 },
20995
20996 updateStartTime: function(startTime) {
20997 this.reset();
20998 },
20999
21000 reset: function() {
21001 this.isEnded = false;
21002 },
21003
21004 getValue: Ext.emptyFn
21005 });
21006
21007 /**
21008 * @private
21009 */
21010 Ext.define('Ext.fx.easing.Linear', {
21011
21012 extend: Ext.fx.easing.Abstract ,
21013
21014 alias: 'easing.linear',
21015
21016 config: {
21017 duration: 0,
21018 endValue: 0
21019 },
21020
21021 updateStartValue: function(startValue) {
21022 this.distance = this.getEndValue() - startValue;
21023 },
21024
21025 updateEndValue: function(endValue) {
21026 this.distance = endValue - this.getStartValue();
21027 },
21028
21029 getValue: function() {
21030 var deltaTime = Ext.Date.now() - this.getStartTime(),
21031 duration = this.getDuration();
21032
21033 if (deltaTime > duration) {
21034 this.isEnded = true;
21035 return this.getEndValue();
21036 }
21037 else {
21038 return this.getStartValue() + ((deltaTime / duration) * this.distance);
21039 }
21040 }
21041 });
21042
21043 /**
21044 * @private
21045 *
21046 * The abstract class. Sub-classes are expected, at the very least, to implement translation logics inside
21047 * the 'translate' method
21048 */
21049 Ext.define('Ext.util.translatable.Abstract', {
21050 extend: Ext.Evented ,
21051
21052
21053
21054 config: {
21055 useWrapper: null,
21056
21057 easing: null,
21058
21059 easingX: null,
21060
21061 easingY: null
21062 },
21063
21064 /**
21065 * @event animationstart
21066 * Fires whenever the animation is started
21067 * @param {Ext.util.translatable.Abstract} this
21068 * @param {Number} x The current translation on the x axis
21069 * @param {Number} y The current translation on the y axis
21070 */
21071
21072 /**
21073 * @event animationframe
21074 * Fires for each animation frame
21075 * @param {Ext.util.translatable.Abstract} this
21076 * @param {Number} x The new translation on the x axis
21077 * @param {Number} y The new translation on the y axis
21078 */
21079
21080 /**
21081 * @event animationend
21082 * Fires whenever the animation is ended
21083 * @param {Ext.util.translatable.Abstract} this
21084 * @param {Number} x The current translation on the x axis
21085 * @param {Number} y The current translation on the y axis
21086 */
21087
21088 x: 0,
21089
21090 y: 0,
21091
21092 activeEasingX: null,
21093
21094 activeEasingY: null,
21095
21096 isAnimating: false,
21097
21098 isTranslatable: true,
21099
21100 constructor: function(config) {
21101 this.initConfig(config);
21102 },
21103
21104 factoryEasing: function(easing) {
21105 return Ext.factory(easing, Ext.fx.easing.Linear, null, 'easing');
21106 },
21107
21108 applyEasing: function(easing) {
21109 if (!this.getEasingX()) {
21110 this.setEasingX(this.factoryEasing(easing));
21111 }
21112
21113 if (!this.getEasingY()) {
21114 this.setEasingY(this.factoryEasing(easing));
21115 }
21116 },
21117
21118 applyEasingX: function(easing) {
21119 return this.factoryEasing(easing);
21120 },
21121
21122 applyEasingY: function(easing) {
21123 return this.factoryEasing(easing);
21124 },
21125
21126 doTranslate: Ext.emptyFn,
21127
21128 translate: function(x, y, animation) {
21129 if (animation) {
21130 return this.translateAnimated(x, y, animation);
21131 }
21132
21133 if (this.isAnimating) {
21134 this.stopAnimation();
21135 }
21136
21137 if (!isNaN(x) && typeof x == 'number') {
21138 this.x = x;
21139 }
21140
21141 if (!isNaN(y) && typeof y == 'number') {
21142 this.y = y;
21143 }
21144 this.doTranslate(x, y);
21145 },
21146
21147 translateAxis: function(axis, value, animation) {
21148 var x, y;
21149
21150 if (axis == 'x') {
21151 x = value;
21152 }
21153 else {
21154 y = value;
21155 }
21156
21157 return this.translate(x, y, animation);
21158 },
21159
21160 animate: function(easingX, easingY) {
21161 this.activeEasingX = easingX;
21162 this.activeEasingY = easingY;
21163
21164 this.isAnimating = true;
21165 this.lastX = null;
21166 this.lastY = null;
21167
21168 Ext.AnimationQueue.start(this.doAnimationFrame, this);
21169
21170 this.fireEvent('animationstart', this, this.x, this.y);
21171 return this;
21172 },
21173
21174 translateAnimated: function(x, y, animation) {
21175 if (!Ext.isObject(animation)) {
21176 animation = {};
21177 }
21178
21179 if (this.isAnimating) {
21180 this.stopAnimation();
21181 }
21182
21183 var now = Ext.Date.now(),
21184 easing = animation.easing,
21185 easingX = (typeof x == 'number') ? (animation.easingX || easing || this.getEasingX() || true) : null,
21186 easingY = (typeof y == 'number') ? (animation.easingY || easing || this.getEasingY() || true) : null;
21187
21188 if (easingX) {
21189 easingX = this.factoryEasing(easingX);
21190 easingX.setStartTime(now);
21191 easingX.setStartValue(this.x);
21192 easingX.setEndValue(x);
21193
21194 if ('duration' in animation) {
21195 easingX.setDuration(animation.duration);
21196 }
21197 }
21198
21199 if (easingY) {
21200 easingY = this.factoryEasing(easingY);
21201 easingY.setStartTime(now);
21202 easingY.setStartValue(this.y);
21203 easingY.setEndValue(y);
21204
21205 if ('duration' in animation) {
21206 easingY.setDuration(animation.duration);
21207 }
21208 }
21209
21210 return this.animate(easingX, easingY);
21211 },
21212
21213 doAnimationFrame: function() {
21214 var me = this,
21215 easingX = me.activeEasingX,
21216 easingY = me.activeEasingY,
21217 now = Date.now(),
21218 x, y;
21219
21220 if (!me.isAnimating) {
21221 return;
21222 }
21223
21224 me.lastRun = now;
21225
21226 if (easingX === null && easingY === null) {
21227 me.stopAnimation();
21228 return;
21229 }
21230
21231 if (easingX !== null) {
21232 me.x = x = Math.round(easingX.getValue());
21233
21234 if (easingX.isEnded) {
21235 me.activeEasingX = null;
21236 me.fireEvent('axisanimationend', me, 'x', x);
21237 }
21238 }
21239 else {
21240 x = me.x;
21241 }
21242
21243 if (easingY !== null) {
21244 me.y = y = Math.round(easingY.getValue());
21245
21246 if (easingY.isEnded) {
21247 me.activeEasingY = null;
21248 me.fireEvent('axisanimationend', me, 'y', y);
21249 }
21250 }
21251 else {
21252 y = me.y;
21253 }
21254
21255 if (me.lastX !== x || me.lastY !== y) {
21256 me.doTranslate(x, y);
21257
21258 me.lastX = x;
21259 me.lastY = y;
21260 }
21261
21262 me.fireEvent('animationframe', me, x, y);
21263 },
21264
21265 stopAnimation: function() {
21266 if (!this.isAnimating) {
21267 return;
21268 }
21269
21270 this.activeEasingX = null;
21271 this.activeEasingY = null;
21272
21273 this.isAnimating = false;
21274
21275 Ext.AnimationQueue.stop(this.doAnimationFrame, this);
21276 this.fireEvent('animationend', this, this.x, this.y);
21277 },
21278
21279 refresh: function() {
21280 this.translate(this.x, this.y);
21281 },
21282
21283 destroy: function() {
21284 if (this.isAnimating) {
21285 this.stopAnimation();
21286 }
21287
21288 this.callParent(arguments);
21289 }
21290 });
21291
21292 /**
21293 * @private
21294 */
21295 Ext.define('Ext.util.translatable.Dom', {
21296 extend: Ext.util.translatable.Abstract ,
21297
21298 config: {
21299 element: null
21300 },
21301
21302 applyElement: function(element) {
21303 if (!element) {
21304 return;
21305 }
21306
21307 return Ext.get(element);
21308 },
21309
21310 updateElement: function() {
21311 this.refresh();
21312 }
21313 });
21314
21315 /**
21316 * @private
21317 *
21318 * CSS Transform implementation
21319 */
21320 Ext.define('Ext.util.translatable.CssTransform', {
21321 extend: Ext.util.translatable.Dom ,
21322
21323 doTranslate: function(x, y) {
21324 var element = this.getElement();
21325 if (!this.isDestroyed && !element.isDestroyed) {
21326 element.translate(x, y);
21327 }
21328 },
21329
21330 destroy: function() {
21331 var element = this.getElement();
21332
21333 if (element && !element.isDestroyed) {
21334 element.dom.style.webkitTransform = null;
21335 }
21336
21337 this.callSuper();
21338 }
21339 });
21340
21341 /**
21342 * @private
21343 *
21344 * Scroll position implementation
21345 */
21346 Ext.define('Ext.util.translatable.ScrollPosition', {
21347 extend: Ext.util.translatable.Dom ,
21348
21349 type: 'scrollposition',
21350
21351 config: {
21352 useWrapper: true
21353 },
21354
21355 getWrapper: function() {
21356 var wrapper = this.wrapper,
21357 element = this.getElement(),
21358 container;
21359
21360 if (!wrapper) {
21361 container = element.getParent();
21362
21363 if (!container) {
21364 return null;
21365 }
21366
21367 if (container.hasCls(Ext.baseCSSPrefix + 'translatable-hboxfix')) {
21368 container = container.getParent();
21369 }
21370
21371 if (this.getUseWrapper()) {
21372 wrapper = element.wrap();
21373 }
21374 else {
21375 wrapper = container;
21376 }
21377
21378 element.addCls('x-translatable');
21379 wrapper.addCls('x-translatable-container');
21380
21381 this.wrapper = wrapper;
21382
21383 wrapper.on('painted', function() {
21384 if (!this.isAnimating) {
21385 this.refresh();
21386 }
21387 }, this);
21388
21389 this.refresh();
21390 }
21391
21392 return wrapper;
21393 },
21394
21395 doTranslate: function(x, y) {
21396 var wrapper = this.getWrapper(),
21397 dom;
21398
21399 if (wrapper) {
21400 dom = wrapper.dom;
21401
21402 if (typeof x == 'number') {
21403 dom.scrollLeft = 500000 - x;
21404 }
21405
21406 if (typeof y == 'number') {
21407 dom.scrollTop = 500000 - y;
21408 }
21409 }
21410 },
21411
21412 destroy: function() {
21413 var element = this.getElement(),
21414 wrapper = this.wrapper;
21415
21416 if (wrapper) {
21417 if (!element.isDestroyed) {
21418 if (this.getUseWrapper()) {
21419 wrapper.doReplaceWith(element);
21420 }
21421 element.removeCls('x-translatable');
21422 }
21423 if (!wrapper.isDestroyed) {
21424 wrapper.removeCls('x-translatable-container');
21425 wrapper.un('painted', 'refresh', this);
21426 }
21427
21428 delete this.wrapper;
21429 delete this._element;
21430 }
21431
21432 this.callSuper();
21433 }
21434
21435 });
21436
21437 /**
21438 * @class Ext.util.translatable.CssPosition
21439 * @private
21440 */
21441
21442 Ext.define('Ext.util.translatable.CssPosition', {
21443 extend: Ext.util.translatable.Dom ,
21444
21445 doTranslate: function(x, y) {
21446 var domStyle = this.getElement().dom.style;
21447
21448 if (typeof x == 'number') {
21449 domStyle.left = x + 'px';
21450 }
21451
21452 if (typeof y == 'number') {
21453 domStyle.top = y + 'px';
21454 }
21455 },
21456
21457 destroy: function() {
21458 var domStyle = this.getElement().dom.style;
21459
21460 domStyle.left = null;
21461 domStyle.top = null;
21462
21463 this.callParent(arguments);
21464 }
21465 });
21466
21467 /**
21468 * The utility class to abstract different implementations to have the best performance when applying 2D translation
21469 * on any DOM element.
21470 *
21471 * @private
21472 */
21473 Ext.define('Ext.util.Translatable', {
21474
21475
21476
21477
21478
21479
21480 constructor: function(config) {
21481 var namespace = Ext.util.translatable;
21482
21483 switch (Ext.browser.getPreferredTranslationMethod(config)) {
21484 case 'scrollposition':
21485 return new namespace.ScrollPosition(config);
21486 case 'csstransform':
21487 return new namespace.CssTransform(config);
21488 case 'cssposition':
21489 return new namespace.CssPosition(config);
21490 }
21491 }
21492 });
21493
21494 /**
21495 * @private
21496 */
21497 Ext.define('Ext.behavior.Translatable', {
21498
21499 extend: Ext.behavior.Behavior ,
21500
21501
21502
21503
21504
21505 setConfig: function(config) {
21506 var translatable = this.translatable,
21507 component = this.component;
21508
21509 if (config) {
21510 if (!translatable) {
21511 this.translatable = translatable = new Ext.util.Translatable(config);
21512 translatable.setElement(component.renderElement);
21513 translatable.on('destroy', 'onTranslatableDestroy', this);
21514 }
21515 else if (Ext.isObject(config)) {
21516 translatable.setConfig(config);
21517 }
21518 }
21519 else if (translatable) {
21520 translatable.destroy();
21521 }
21522
21523 return this;
21524 },
21525
21526 getTranslatable: function() {
21527 return this.translatable;
21528 },
21529
21530 onTranslatableDestroy: function() {
21531 delete this.translatable;
21532 },
21533
21534 onComponentDestroy: function() {
21535 var translatable = this.translatable;
21536
21537 if (translatable) {
21538 translatable.destroy();
21539 }
21540 }
21541 });
21542
21543 /**
21544 * A core util class to bring Draggable behavior to a Component. This class is specifically designed only for
21545 * absolutely positioned elements starting from top: 0, left: 0. The initialOffset can then be set via configuration
21546 * to have the elements in a different position.
21547 */
21548 Ext.define('Ext.util.Draggable', {
21549 isDraggable: true,
21550
21551 mixins: [
21552 Ext.mixin.Observable
21553 ],
21554
21555
21556
21557
21558
21559 /**
21560 * @event dragstart
21561 * @preventable initDragStart
21562 * Fires whenever the component starts to be dragged
21563 * @param {Ext.util.Draggable} this
21564 * @param {Ext.event.Event} e the event object
21565 * @param {Number} offsetX The current offset value on the x axis
21566 * @param {Number} offsetY The current offset value on the y axis
21567 */
21568
21569 /**
21570 * @event drag
21571 * Fires whenever the component is dragged
21572 * @param {Ext.util.Draggable} this
21573 * @param {Ext.event.Event} e the event object
21574 * @param {Number} offsetX The new offset value on the x axis
21575 * @param {Number} offsetY The new offset value on the y axis
21576 */
21577
21578 /**
21579 * @event dragend
21580 * Fires whenever the component is dragged
21581 * @param {Ext.util.Draggable} this
21582 * @param {Ext.event.Event} e the event object
21583 * @param {Number} offsetX The current offset value on the x axis
21584 * @param {Number} offsetY The current offset value on the y axis
21585 */
21586
21587 config: {
21588 cls: Ext.baseCSSPrefix + 'draggable',
21589
21590 draggingCls: Ext.baseCSSPrefix + 'dragging',
21591
21592 element: null,
21593
21594 constraint: 'container',
21595
21596 disabled: null,
21597
21598 /**
21599 * @cfg {String} direction
21600 * Possible values: 'vertical', 'horizontal', or 'both'
21601 * @accessor
21602 */
21603 direction: 'both',
21604
21605 /**
21606 * @cfg {Object/Number} initialOffset
21607 * The initial draggable offset. When specified as Number,
21608 * both x and y will be set to that value.
21609 */
21610 initialOffset: {
21611 x: 0,
21612 y: 0
21613 },
21614
21615 translatable: {}
21616 },
21617
21618 DIRECTION_BOTH: 'both',
21619
21620 DIRECTION_VERTICAL: 'vertical',
21621
21622 DIRECTION_HORIZONTAL: 'horizontal',
21623
21624 defaultConstraint: {
21625 min: { x: -Infinity, y: -Infinity },
21626 max: { x: Infinity, y: Infinity }
21627 },
21628
21629 containerWidth: 0,
21630
21631 containerHeight: 0,
21632
21633 width: 0,
21634
21635 height: 0,
21636
21637 /**
21638 * Creates new Draggable.
21639 * @param {Object} config The configuration object for this Draggable.
21640 */
21641 constructor: function(config) {
21642 var element;
21643
21644 this.extraConstraint = {};
21645
21646 this.initialConfig = config;
21647
21648 this.offset = {
21649 x: 0,
21650 y: 0
21651 };
21652
21653 this.listeners = {
21654 dragstart: 'onDragStart',
21655 drag : 'onDrag',
21656 dragend : 'onDragEnd',
21657 resize : 'onElementResize',
21658 touchstart : 'onPress',
21659 touchend : 'onRelease',
21660 scope: this
21661 };
21662
21663 if (config && config.element) {
21664 element = config.element;
21665 delete config.element;
21666
21667 this.setElement(element);
21668 }
21669
21670 return this;
21671 },
21672
21673 applyElement: function(element) {
21674 if (!element) {
21675 return;
21676 }
21677
21678 return Ext.get(element);
21679 },
21680
21681 updateElement: function(element) {
21682 element.on(this.listeners);
21683
21684 this.initConfig(this.initialConfig);
21685 },
21686
21687 updateInitialOffset: function(initialOffset) {
21688 if (typeof initialOffset == 'number') {
21689 initialOffset = {
21690 x: initialOffset,
21691 y: initialOffset
21692 };
21693 }
21694
21695 var offset = this.offset,
21696 x, y;
21697
21698 offset.x = x = initialOffset.x;
21699 offset.y = y = initialOffset.y;
21700
21701 this.getTranslatable().translate(x, y);
21702 },
21703
21704 updateCls: function(cls) {
21705 this.getElement().addCls(cls);
21706 },
21707
21708 applyTranslatable: function(translatable, currentInstance) {
21709 translatable = Ext.factory(translatable, Ext.util.Translatable, currentInstance);
21710 if (translatable) {
21711 translatable.setElement(this.getElement());
21712 }
21713
21714 return translatable;
21715 },
21716
21717 setExtraConstraint: function(constraint) {
21718 this.extraConstraint = constraint || {};
21719
21720 this.refreshConstraint();
21721
21722 return this;
21723 },
21724
21725 addExtraConstraint: function(constraint) {
21726 Ext.merge(this.extraConstraint, constraint);
21727
21728 this.refreshConstraint();
21729
21730 return this;
21731 },
21732
21733 applyConstraint: function(newConstraint) {
21734 this.currentConstraint = newConstraint;
21735
21736 if (!newConstraint) {
21737 newConstraint = this.defaultConstraint;
21738 }
21739
21740 if (newConstraint === 'container') {
21741 return Ext.merge(this.getContainerConstraint(), this.extraConstraint);
21742 }
21743
21744 return Ext.merge({}, this.extraConstraint, newConstraint);
21745 },
21746
21747 updateConstraint: function() {
21748 this.refreshOffset();
21749 },
21750
21751 getContainerConstraint: function() {
21752 var container = this.getContainer(),
21753 element = this.getElement();
21754
21755 if (!container || !element.dom) {
21756 return this.defaultConstraint;
21757 }
21758
21759 return {
21760 min: { x: 0, y: 0 },
21761 max: { x: this.containerWidth - this.width, y: this.containerHeight - this.height }
21762 };
21763 },
21764
21765 getContainer: function() {
21766 var container = this.container;
21767
21768 if (!container) {
21769 container = this.getElement().getParent();
21770
21771 if (container) {
21772 this.container = container;
21773
21774 container.on({
21775 resize: 'onContainerResize',
21776 destroy: 'onContainerDestroy',
21777 scope: this
21778 });
21779 }
21780 }
21781
21782 return container;
21783 },
21784
21785 onElementResize: function(element, info) {
21786 this.width = info.width;
21787 this.height = info.height;
21788
21789 this.refresh();
21790 },
21791
21792 onContainerResize: function(container, info) {
21793 this.containerWidth = info.width;
21794 this.containerHeight = info.height;
21795
21796 this.refresh();
21797 },
21798
21799 onContainerDestroy: function() {
21800 delete this.container;
21801 delete this.containerSizeMonitor;
21802 },
21803
21804 detachListeners: function() {
21805 this.getElement().un(this.listeners);
21806 },
21807
21808 isAxisEnabled: function(axis) {
21809 var direction = this.getDirection();
21810
21811 if (axis === 'x') {
21812 return (direction === this.DIRECTION_BOTH || direction === this.DIRECTION_HORIZONTAL);
21813 }
21814
21815 return (direction === this.DIRECTION_BOTH || direction === this.DIRECTION_VERTICAL);
21816 },
21817
21818 onPress: function(e) {
21819 this.fireAction('touchstart', [this, e]);
21820 },
21821
21822 onRelease: function(e) {
21823 this.fireAction('touchend', [this, e]);
21824 },
21825
21826 onDragStart: function(e) {
21827 if (this.getDisabled()) {
21828 return false;
21829 }
21830 var offset = this.offset;
21831
21832 this.fireAction('dragstart', [this, e, offset.x, offset.y], this.initDragStart);
21833 },
21834
21835 initDragStart: function(me, e, offsetX, offsetY) {
21836 this.dragStartOffset = {
21837 x: offsetX,
21838 y: offsetY
21839 };
21840
21841 this.isDragging = true;
21842
21843 this.getElement().addCls(this.getDraggingCls());
21844 },
21845
21846 onDrag: function(e) {
21847 if (!this.isDragging) {
21848 return;
21849 }
21850
21851 var startOffset = this.dragStartOffset;
21852
21853 this.fireAction('drag', [this, e, startOffset.x + e.deltaX, startOffset.y + e.deltaY], this.doDrag);
21854 },
21855
21856 doDrag: function(me, e, offsetX, offsetY) {
21857 me.setOffset(offsetX, offsetY);
21858 },
21859
21860 onDragEnd: function(e) {
21861 if (!this.isDragging) {
21862 return;
21863 }
21864
21865 this.onDrag(e);
21866
21867 this.isDragging = false;
21868
21869 this.getElement().removeCls(this.getDraggingCls());
21870
21871 this.fireEvent('dragend', this, e, this.offset.x, this.offset.y);
21872 },
21873
21874 setOffset: function(x, y, animation) {
21875 var currentOffset = this.offset,
21876 constraint = this.getConstraint(),
21877 minOffset = constraint.min,
21878 maxOffset = constraint.max,
21879 min = Math.min,
21880 max = Math.max;
21881
21882 if (this.isAxisEnabled('x') && typeof x == 'number') {
21883 x = min(max(x, minOffset.x), maxOffset.x);
21884 }
21885 else {
21886 x = currentOffset.x;
21887 }
21888
21889 if (this.isAxisEnabled('y') && typeof y == 'number') {
21890 y = min(max(y, minOffset.y), maxOffset.y);
21891 }
21892 else {
21893 y = currentOffset.y;
21894 }
21895
21896 currentOffset.x = x;
21897 currentOffset.y = y;
21898
21899 this.getTranslatable().translate(x, y, animation);
21900 },
21901
21902 getOffset: function() {
21903 return this.offset;
21904 },
21905
21906 refreshConstraint: function() {
21907 this.setConstraint(this.currentConstraint);
21908 },
21909
21910 refreshOffset: function() {
21911 var offset = this.offset;
21912
21913 this.setOffset(offset.x, offset.y);
21914 },
21915
21916 refresh: function() {
21917 this.refreshConstraint();
21918 this.getTranslatable().refresh();
21919 this.refreshOffset();
21920 },
21921
21922 /**
21923 * Enable the Draggable.
21924 * @return {Ext.util.Draggable} This Draggable instance
21925 */
21926 enable: function() {
21927 return this.setDisabled(false);
21928 },
21929
21930 /**
21931 * Disable the Draggable.
21932 * @return {Ext.util.Draggable} This Draggable instance
21933 */
21934 disable: function() {
21935 return this.setDisabled(true);
21936 },
21937
21938 destroy: function() {
21939 var translatable = this.getTranslatable();
21940
21941 var element = this.getElement();
21942 if (element && !element.isDestroyed) {
21943 element.removeCls(this.getCls());
21944 }
21945
21946 this.detachListeners();
21947
21948 if (translatable) {
21949 translatable.destroy();
21950 }
21951 }
21952
21953 }, function() {
21954 });
21955
21956
21957 /**
21958 * @private
21959 */
21960 Ext.define('Ext.behavior.Draggable', {
21961
21962 extend: Ext.behavior.Behavior ,
21963
21964
21965
21966
21967
21968 setConfig: function(config) {
21969 var draggable = this.draggable,
21970 component = this.component;
21971
21972 if (config) {
21973 if (!draggable) {
21974 component.setTranslatable(config.translatable);
21975 this.draggable = draggable = new Ext.util.Draggable(config);
21976 draggable.setTranslatable(component.getTranslatable());
21977 draggable.setElement(component.renderElement);
21978 draggable.on('destroy', 'onDraggableDestroy', this);
21979
21980 component.on(this.listeners);
21981 }
21982 else if (Ext.isObject(config)) {
21983 draggable.setConfig(config);
21984 }
21985 }
21986 else if (draggable) {
21987 draggable.destroy();
21988 }
21989
21990 return this;
21991 },
21992
21993 getDraggable: function() {
21994 return this.draggable;
21995 },
21996
21997 onDraggableDestroy: function() {
21998 delete this.draggable;
21999 },
22000
22001 onComponentDestroy: function() {
22002 var draggable = this.draggable;
22003
22004 if (draggable) {
22005 draggable.destroy();
22006 }
22007 }
22008 });
22009
22010 (function(clsPrefix) {
22011
22012 /**
22013 * Most of the visual classes you interact with in Sencha Touch are Components. Every Component in Sencha Touch is a
22014 * subclass of Ext.Component, which means they can all:
22015 *
22016 * * Render themselves onto the page using a template
22017 * * Show and hide themselves at any time
22018 * * Center themselves on the screen
22019 * * Enable and disable themselves
22020 *
22021 * They can also do a few more advanced things:
22022 *
22023 * * Float above other components (windows, message boxes and overlays)
22024 * * Change size and position on the screen with animation
22025 * * Dock other Components inside themselves (useful for toolbars)
22026 * * Align to other components, allow themselves to be dragged around, make their content scrollable & more
22027 *
22028 * ## Available Components
22029 *
22030 * There are many components available in Sencha Touch, separated into 4 main groups:
22031 *
22032 * ### Navigation components
22033 * * {@link Ext.Toolbar}
22034 * * {@link Ext.Button}
22035 * * {@link Ext.TitleBar}
22036 * * {@link Ext.SegmentedButton}
22037 * * {@link Ext.Title}
22038 * * {@link Ext.Spacer}
22039 *
22040 * ### Store-bound components
22041 * * {@link Ext.dataview.DataView}
22042 * * {@link Ext.Carousel}
22043 * * {@link Ext.List}
22044 * * {@link Ext.NestedList}
22045 *
22046 * ### Form components
22047 * * {@link Ext.form.Panel}
22048 * * {@link Ext.form.FieldSet}
22049 * * {@link Ext.field.Checkbox}
22050 * * {@link Ext.field.Hidden}
22051 * * {@link Ext.field.Slider}
22052 * * {@link Ext.field.Text}
22053 * * {@link Ext.picker.Picker}
22054 * * {@link Ext.picker.Date}
22055 *
22056 * ### General components
22057 * * {@link Ext.Panel}
22058 * * {@link Ext.tab.Panel}
22059 * * {@link Ext.Viewport Ext.Viewport}
22060 * * {@link Ext.Img}
22061 * * {@link Ext.Map}
22062 * * {@link Ext.Audio}
22063 * * {@link Ext.Video}
22064 * * {@link Ext.Sheet}
22065 * * {@link Ext.ActionSheet}
22066 * * {@link Ext.MessageBox}
22067 *
22068 *
22069 * ## Instantiating Components
22070 *
22071 * Components are created the same way as all other classes in Sencha Touch - using Ext.create. Here's how we can
22072 * create a Text field:
22073 *
22074 * var panel = Ext.create('Ext.Panel', {
22075 * html: 'This is my panel'
22076 * });
22077 *
22078 * This will create a {@link Ext.Panel Panel} instance, configured with some basic HTML content. A Panel is just a
22079 * simple Component that can render HTML and also contain other items. In this case we've created a Panel instance but
22080 * it won't show up on the screen yet because items are not rendered immediately after being instantiated. This allows
22081 * us to create some components and move them around before rendering and laying them out, which is a good deal faster
22082 * than moving them after rendering.
22083 *
22084 * To show this panel on the screen now we can simply add it to the global Viewport:
22085 *
22086 * Ext.Viewport.add(panel);
22087 *
22088 * Panels are also Containers, which means they can contain other Components, arranged by a layout. Let's revisit the
22089 * above example now, this time creating a panel with two child Components and a hbox layout:
22090 *
22091 * @example
22092 * var panel = Ext.create('Ext.Panel', {
22093 * layout: 'hbox',
22094 *
22095 * items: [
22096 * {
22097 * xtype: 'panel',
22098 * flex: 1,
22099 * html: 'Left Panel, 1/3rd of total size',
22100 * style: 'background-color: #5E99CC;'
22101 * },
22102 * {
22103 * xtype: 'panel',
22104 * flex: 2,
22105 * html: 'Right Panel, 2/3rds of total size',
22106 * style: 'background-color: #759E60;'
22107 * }
22108 * ]
22109 * });
22110 *
22111 * Ext.Viewport.add(panel);
22112 *
22113 * This time we created 3 Panels - the first one is created just as before but the inner two are declared inline using
22114 * an xtype. Xtype is a convenient way of creating Components without having to go through the process of using
22115 * Ext.create and specifying the full class name, instead you can just provide the xtype for the class inside an object
22116 * and the framework will create the components for you.
22117 *
22118 * We also specified a layout for the top level panel - in this case hbox, which splits the horizontal width of the
22119 * parent panel based on the 'flex' of each child. For example, if the parent Panel above is 300px wide then the first
22120 * child will be flexed to 100px wide and the second to 200px because the first one was given `flex: 1` and the second
22121 * `flex: 2`.
22122 *
22123 * ## Using xtype
22124 *
22125 * xtype is an easy way to create Components without using the full class name. This is especially useful when creating
22126 * a {@link Ext.Container Container} that contains child Components. An xtype is simply a shorthand way of specifying a
22127 * Component - for example you can use `xtype: 'panel'` instead of typing out Ext.panel.Panel.
22128 *
22129 * Sample usage:
22130 *
22131 * @example miniphone
22132 * Ext.create('Ext.Container', {
22133 * fullscreen: true,
22134 * layout: 'fit',
22135 *
22136 * items: [
22137 * {
22138 * xtype: 'panel',
22139 * html: 'This panel is created by xtype'
22140 * },
22141 * {
22142 * xtype: 'toolbar',
22143 * title: 'So is the toolbar',
22144 * docked: 'top'
22145 * }
22146 * ]
22147 * });
22148 *
22149 *
22150 * ### Common xtypes
22151 *
22152 * These are the xtypes that are most commonly used. For an exhaustive list please see the
22153 * [Components Guide](../../../core_concepts/components.html).
22154 *
22155 * <pre>
22156 xtype Class
22157 ----------------- ---------------------
22158 actionsheet Ext.ActionSheet
22159 audio Ext.Audio
22160 button Ext.Button
22161 image Ext.Img
22162 label Ext.Label
22163 loadmask Ext.LoadMask
22164 map Ext.Map
22165 panel Ext.Panel
22166 segmentedbutton Ext.SegmentedButton
22167 sheet Ext.Sheet
22168 spacer Ext.Spacer
22169 titlebar Ext.TitleBar
22170 toolbar Ext.Toolbar
22171 video Ext.Video
22172 carousel Ext.carousel.Carousel
22173 navigationview Ext.navigation.View
22174 datepicker Ext.picker.Date
22175 picker Ext.picker.Picker
22176 slider Ext.slider.Slider
22177 thumb Ext.slider.Thumb
22178 tabpanel Ext.tab.Panel
22179 viewport Ext.viewport.Default
22180
22181 DataView Components
22182 ---------------------------------------------
22183 dataview Ext.dataview.DataView
22184 list Ext.dataview.List
22185 nestedlist Ext.dataview.NestedList
22186
22187 Form Components
22188 ---------------------------------------------
22189 checkboxfield Ext.field.Checkbox
22190 datepickerfield Ext.field.DatePicker
22191 emailfield Ext.field.Email
22192 hiddenfield Ext.field.Hidden
22193 numberfield Ext.field.Number
22194 passwordfield Ext.field.Password
22195 radiofield Ext.field.Radio
22196 searchfield Ext.field.Search
22197 selectfield Ext.field.Select
22198 sliderfield Ext.field.Slider
22199 spinnerfield Ext.field.Spinner
22200 textfield Ext.field.Text
22201 textareafield Ext.field.TextArea
22202 togglefield Ext.field.Toggle
22203 urlfield Ext.field.Url
22204 fieldset Ext.form.FieldSet
22205 formpanel Ext.form.Panel
22206 * </pre>
22207 *
22208 * ## Configuring Components
22209 *
22210 * Whenever you create a new Component you can pass in configuration options. All of the configurations for a given
22211 * Component are listed in the "Config options" section of its class docs page. You can pass in any number of
22212 * configuration options when you instantiate the Component, and modify any of them at any point later. For example, we
22213 * can easily modify the {@link Ext.Panel#html html content} of a Panel after creating it:
22214 *
22215 * @example miniphone
22216 * // we can configure the HTML when we instantiate the Component
22217 * var panel = Ext.create('Ext.Panel', {
22218 * fullscreen: true,
22219 * html: 'This is a Panel'
22220 * });
22221 *
22222 * // we can update the HTML later using the setHtml method:
22223 * panel.setHtml('Some new HTML');
22224 *
22225 * // we can retrieve the current HTML using the getHtml method:
22226 * Ext.Msg.alert(panel.getHtml()); // displays "Some new HTML"
22227 *
22228 * Every config has a getter method and a setter method - these are automatically generated and always follow the same
22229 * pattern. For example, a config called `html` will receive `getHtml` and `setHtml` methods, a config called `defaultType`
22230 * will receive `getDefaultType` and `setDefaultType` methods, and so on.
22231 *
22232 * ## Further Reading
22233 *
22234 * See the [Component & Container Guide](../../../core_concepts/components.html) for more information, and check out the
22235 * {@link Ext.Container} class docs also.
22236 *
22237 */
22238 Ext.define('Ext.Component', {
22239
22240 extend: Ext.AbstractComponent ,
22241
22242 alternateClassName: 'Ext.lib.Component',
22243
22244 mixins: [ Ext.mixin.Traversable ],
22245
22246
22247
22248
22249
22250
22251
22252
22253
22254 /**
22255 * @cfg {String} xtype
22256 * The `xtype` configuration option can be used to optimize Component creation and rendering. It serves as a
22257 * shortcut to the full component name. For example, the component `Ext.button.Button` has an xtype of `button`.
22258 *
22259 * You can define your own xtype on a custom {@link Ext.Component component} by specifying the
22260 * {@link Ext.Class#alias alias} config option with a prefix of `widget`. For example:
22261 *
22262 * Ext.define('PressMeButton', {
22263 * extend: 'Ext.button.Button',
22264 * alias: 'widget.pressmebutton',
22265 * text: 'Press Me'
22266 * });
22267 *
22268 * Any Component can be created implicitly as an object config with an xtype specified, allowing it to be
22269 * declared and passed into the rendering pipeline without actually being instantiated as an object. Not only is
22270 * rendering deferred, but the actual creation of the object itself is also deferred, saving memory and resources
22271 * until they are actually needed. In complex, nested layouts containing many Components, this can make a
22272 * noticeable improvement in performance.
22273 *
22274 * // Explicit creation of contained Components:
22275 * var panel = new Ext.Panel({
22276 * // ...
22277 * items: [
22278 * Ext.create('Ext.button.Button', {
22279 * text: 'OK'
22280 * })
22281 * ]
22282 * });
22283 *
22284 * // Implicit creation using xtype:
22285 * var panel = new Ext.Panel({
22286 * // ...
22287 * items: [{
22288 * xtype: 'button',
22289 * text: 'OK'
22290 * }]
22291 * });
22292 *
22293 * In the first example, the button will always be created immediately during the panel's initialization. With
22294 * many added Components, this approach could potentially slow the rendering of the page. In the second example,
22295 * the button will not be created or rendered until the panel is actually displayed in the browser. If the panel
22296 * is never displayed (for example, if it is a tab that remains hidden) then the button will never be created and
22297 * will never consume any resources whatsoever.
22298 */
22299 xtype: 'component',
22300
22301 observableType: 'component',
22302
22303 cachedConfig: {
22304 /**
22305 * @cfg {String} baseCls
22306 * The base CSS class to apply to this component's element. This will also be prepended to
22307 * other elements within this component. To add specific styling for sub-classes, use the {@link #cls} config.
22308 * @accessor
22309 */
22310 baseCls: null,
22311
22312 /**
22313 * @cfg {String/String[]} cls The CSS class to add to this component's element, in addition to the {@link #baseCls}
22314 * @accessor
22315 */
22316 cls: null,
22317
22318 /**
22319 * @cfg {String} [floatingCls="x-floating"] The CSS class to add to this component when it is floatable.
22320 * @accessor
22321 */
22322 floatingCls: clsPrefix + 'floating',
22323
22324 /**
22325 * @cfg {String} [hiddenCls="x-item-hidden"] The CSS class to add to the component when it is hidden
22326 * @accessor
22327 */
22328 hiddenCls: clsPrefix + 'item-hidden',
22329
22330 /**
22331 * @cfg {String} ui The ui to be used on this Component
22332 */
22333 ui: null,
22334
22335 /**
22336 * @cfg {Number/String} margin The margin to use on this Component. Can be specified as a number (in which case
22337 * all edges get the same margin) or a CSS string like '5 10 10 10'
22338 * @accessor
22339 */
22340 margin: null,
22341
22342 /**
22343 * @cfg {Number/String} padding The padding to use on this Component. Can be specified as a number (in which
22344 * case all edges get the same padding) or a CSS string like '5 10 10 10'
22345 * @accessor
22346 */
22347 padding: null,
22348
22349 /**
22350 * @cfg {Number/String} border The border width to use on this Component. Can be specified as a number (in which
22351 * case all edges get the same border width) or a CSS string like '5 10 10 10'.
22352 *
22353 * Please note that this will not add
22354 * a `border-color` or `border-style` CSS property to the component; you must do that manually using either CSS or
22355 * the {@link #style} configuration.
22356 *
22357 * ## Using {@link #style}:
22358 *
22359 * Ext.Viewport.add({
22360 * centered: true,
22361 * width: 100,
22362 * height: 100,
22363 *
22364 * border: 3,
22365 * style: 'border-color: blue; border-style: solid;'
22366 * // ...
22367 * });
22368 *
22369 * ## Using CSS:
22370 *
22371 * Ext.Viewport.add({
22372 * centered: true,
22373 * width: 100,
22374 * height: 100,
22375 *
22376 * border: 3,
22377 * cls: 'my-component'
22378 * // ...
22379 * });
22380 *
22381 * And your CSS file:
22382 *
22383 * .my-component {
22384 * border-color: red;
22385 * border-style: solid;
22386 * }
22387 *
22388 * @accessor
22389 */
22390 border: null,
22391
22392 /**
22393 * @cfg {String} [styleHtmlCls="x-html"]
22394 * The class that is added to the content target when you set `styleHtmlContent` to `true`.
22395 * @accessor
22396 */
22397 styleHtmlCls: clsPrefix + 'html',
22398
22399 /**
22400 * @cfg {Boolean} [styleHtmlContent=false]
22401 * `true` to automatically style the HTML inside the content target of this component (body for panels).
22402 * @accessor
22403 */
22404 styleHtmlContent: null
22405 },
22406
22407 eventedConfig: {
22408 /**
22409 * @cfg {Number} flex
22410 * The flex of this item *if* this item item is inside a {@link Ext.layout.HBox} or {@link Ext.layout.VBox}
22411 * layout.
22412 *
22413 * You can also update the flex of a component dynamically using the {@link Ext.layout.FlexBox#setItemFlex}
22414 * method.
22415 */
22416 flex: null,
22417
22418 /**
22419 * @cfg {Number/String} left
22420 * The absolute left position of this Component; must be a valid CSS length value, e.g: `300`, `100px`, `30%`, etc.
22421 * Explicitly setting this value will make this Component become 'floating', which means its layout will no
22422 * longer be affected by the Container that it resides in.
22423 * @accessor
22424 * @evented
22425 */
22426 left: null,
22427
22428 /**
22429 * @cfg {Number/String} top
22430 * The absolute top position of this Component; must be a valid CSS length value, e.g: `300`, `100px`, `30%`, etc.
22431 * Explicitly setting this value will make this Component become 'floating', which means its layout will no
22432 * longer be affected by the Container that it resides in.
22433 * @accessor
22434 * @evented
22435 */
22436 top: null,
22437
22438 /**
22439 * @cfg {Number/String} right
22440 * The absolute right position of this Component; must be a valid CSS length value, e.g: `300`, `100px`, `30%`, etc.
22441 * Explicitly setting this value will make this Component become 'floating', which means its layout will no
22442 * longer be affected by the Container that it resides in.
22443 * @accessor
22444 * @evented
22445 */
22446 right: null,
22447
22448 /**
22449 * @cfg {Number/String} bottom
22450 * The absolute bottom position of this Component; must be a valid CSS length value, e.g: `300`, `100px`, `30%`, etc.
22451 * Explicitly setting this value will make this Component become 'floating', which means its layout will no
22452 * longer be affected by the Container that it resides in.
22453 * @accessor
22454 * @evented
22455 */
22456 bottom: null,
22457
22458 /**
22459 * @cfg {Number/String} width
22460 * The width of this Component; must be a valid CSS length value, e.g: `300`, `100px`, `30%`, etc.
22461 * By default, if this is not explicitly set, this Component's element will simply have its own natural size.
22462 * If set to `auto`, it will set the width to `null` meaning it will have its own natural size.
22463 * @accessor
22464 * @evented
22465 */
22466 width: null,
22467
22468 /**
22469 * @cfg {Number/String} height
22470 * The height of this Component; must be a valid CSS length value, e.g: `300`, `100px`, `30%`, etc.
22471 * By default, if this is not explicitly set, this Component's element will simply have its own natural size.
22472 * If set to `auto`, it will set the width to `null` meaning it will have its own natural size.
22473 * @accessor
22474 * @evented
22475 */
22476 height: null,
22477
22478 /**
22479 * @cfg {Number/String} minWidth
22480 * The minimum width of this Component; must be a valid CSS length value, e.g: `300`, `100px`, `30%`, etc.
22481 * If set to `auto`, it will set the width to `null` meaning it will have its own natural size.
22482 * @accessor
22483 * @evented
22484 */
22485 minWidth: null,
22486
22487 /**
22488 * @cfg {Number/String} minHeight
22489 * The minimum height of this Component; must be a valid CSS length value, e.g: `300`, `100px`, `30%`, etc.
22490 * If set to `auto`, it will set the width to `null` meaning it will have its own natural size.
22491 * @accessor
22492 * @evented
22493 */
22494 minHeight: null,
22495
22496 /**
22497 * @cfg {Number/String} maxWidth
22498 * The maximum width of this Component; must be a valid CSS length value, e.g: `300`, `100px`, `30%`, etc.
22499 * If set to `auto`, it will set the width to `null` meaning it will have its own natural size.
22500 * Note that this config will not apply if the Component is 'floating' (absolutely positioned or centered)
22501 * @accessor
22502 * @evented
22503 */
22504 maxWidth: null,
22505
22506 /**
22507 * @cfg {Number/String} maxHeight
22508 * The maximum height of this Component; must be a valid CSS length value, e.g: `300`, `100px`, `30%`, etc.
22509 * If set to `auto`, it will set the width to `null` meaning it will have its own natural size.
22510 * Note that this config will not apply if the Component is 'floating' (absolutely positioned or centered)
22511 * @accessor
22512 * @evented
22513 */
22514 maxHeight: null,
22515
22516 /**
22517 * @cfg {String} docked
22518 * The dock position of this component in its container. Can be `left`, `top`, `right` or `bottom`.
22519 *
22520 * __Notes__
22521 *
22522 * You must use a HTML5 doctype for {@link #docked} `bottom` to work. To do this, simply add the following code to the HTML file:
22523 *
22524 * <!doctype html>
22525 *
22526 * So your index.html file should look a little like this:
22527 *
22528 * <!doctype html>
22529 * <html>
22530 * <head>
22531 * <title>MY application title</title>
22532 * ...
22533 *
22534 * @accessor
22535 * @evented
22536 */
22537 docked: null,
22538
22539 /**
22540 * @cfg {Boolean} centered
22541 * Whether or not this Component is absolutely centered inside its Container
22542 * @accessor
22543 * @evented
22544 */
22545 centered: null,
22546
22547 /**
22548 * @cfg {Boolean} hidden
22549 * Whether or not this Component is hidden (its CSS `display` property is set to `none`)
22550 * @accessor
22551 * @evented
22552 */
22553 hidden: null,
22554
22555 /**
22556 * @cfg {Boolean} disabled
22557 * Whether or not this component is disabled
22558 * @accessor
22559 * @evented
22560 */
22561 disabled: null
22562 },
22563
22564 config: {
22565 /**
22566 * @cfg {String/Object} style Optional CSS styles that will be rendered into an inline style attribute when the
22567 * Component is rendered.
22568 *
22569 * You can pass either a string syntax:
22570 *
22571 * style: 'background:red'
22572 *
22573 * Or by using an object:
22574 *
22575 * style: {
22576 * background: 'red'
22577 * }
22578 *
22579 * When using the object syntax, you can define CSS Properties by using a string:
22580 *
22581 * style: {
22582 * 'border-left': '1px solid red'
22583 * }
22584 *
22585 * Although the object syntax is much easier to read, we suggest you to use the string syntax for better performance.
22586 *
22587 * @accessor
22588 */
22589 style: null,
22590
22591 /**
22592 * @cfg {String/Ext.Element/HTMLElement} html Optional HTML content to render inside this Component, or a reference
22593 * to an existing element on the page.
22594 * @accessor
22595 */
22596 html: null,
22597
22598 /**
22599 * @cfg {Object} draggable Configuration options to make this Component draggable
22600 * @accessor
22601 */
22602 draggable: null,
22603
22604 /**
22605 * @cfg {Object} translatable
22606 * @private
22607 * @accessor
22608 */
22609 translatable: null,
22610
22611 /**
22612 * @cfg {Ext.Element} renderTo Optional element to render this Component to. Usually this is not needed because
22613 * a Component is normally full screen or automatically rendered inside another {@link Ext.Container Container}
22614 * @accessor
22615 */
22616 renderTo: null,
22617
22618 /**
22619 * @cfg {Number} zIndex The z-index to give this Component when it is rendered
22620 * @accessor
22621 */
22622 zIndex: null,
22623
22624 /**
22625 * @cfg {String/String[]/Ext.Template/Ext.XTemplate[]} tpl
22626 * A {@link String}, {@link Ext.Template}, {@link Ext.XTemplate} or an {@link Array} of strings to form an {@link Ext.XTemplate}.
22627 * Used in conjunction with the {@link #data} and {@link #tplWriteMode} configurations.
22628 *
22629 * __Note__
22630 * The {@link #data} configuration _must_ be set for any content to be shown in the component when using this configuration.
22631 * @accessor
22632 */
22633 tpl: null,
22634
22635 /**
22636 * @cfg {String/Mixed} enterAnimation
22637 * Animation effect to apply when the Component is being shown. Typically you want to use an
22638 * inbound animation type such as 'fadeIn' or 'slideIn'.
22639 * @deprecated 2.0.0 Please use {@link #showAnimation} instead.
22640 * @accessor
22641 */
22642 enterAnimation: null,
22643
22644 /**
22645 * @cfg {String/Mixed} exitAnimation
22646 * Animation effect to apply when the Component is being hidden.
22647 * @deprecated 2.0.0 Please use {@link #hideAnimation} instead. Typically you want to use an
22648 * outbound animation type such as 'fadeOut' or 'slideOut'.
22649 * @accessor
22650 */
22651 exitAnimation: null,
22652
22653 /**
22654 * @cfg {String/Mixed} showAnimation
22655 * Animation effect to apply when the Component is being shown. Typically you want to use an
22656 * inbound animation type such as 'fadeIn' or 'slideIn'. For more animations, check the {@link Ext.fx.Animation#type} config.
22657 * @accessor
22658 */
22659 showAnimation: null,
22660
22661 /**
22662 * @cfg {String/Mixed} hideAnimation
22663 * Animation effect to apply when the Component is being hidden. Typically you want to use an
22664 * outbound animation type such as 'fadeOut' or 'slideOut'. For more animations, check the {@link Ext.fx.Animation#type} config.
22665 * @accessor
22666 */
22667 hideAnimation: null,
22668
22669 /**
22670 * @cfg {String} tplWriteMode The Ext.(X)Template method to use when
22671 * updating the content area of the Component.
22672 * Valid modes are:
22673 *
22674 * - append
22675 * - insertAfter
22676 * - insertBefore
22677 * - insertFirst
22678 * - overwrite
22679 * @accessor
22680 */
22681 tplWriteMode: 'overwrite',
22682
22683 /**
22684 * @cfg {Object} data
22685 * The initial set of data to apply to the `{@link #tpl}` to
22686 * update the content area of the Component.
22687 * @accessor
22688 */
22689 data: null,
22690
22691 /**
22692 * @cfg {String} [disabledCls="x-item-disabled"] The CSS class to add to the component when it is disabled
22693 * @accessor
22694 */
22695 disabledCls: clsPrefix + 'item-disabled',
22696
22697 /**
22698 * @cfg {Ext.Element/HTMLElement/String} contentEl The configured element will automatically be
22699 * added as the content of this component. When you pass a string, we expect it to be an element id.
22700 * If the content element is hidden, we will automatically show it.
22701 * @accessor
22702 */
22703 contentEl: null,
22704
22705 /**
22706 * @cfg {String} id
22707 * The **unique id of this component instance.**
22708 *
22709 * It should not be necessary to use this configuration except for singleton objects in your application. Components
22710 * created with an id may be accessed globally using {@link Ext#getCmp Ext.getCmp}.
22711 *
22712 * Instead of using assigned ids, use the {@link #itemId} config, and {@link Ext.ComponentQuery ComponentQuery}
22713 * which provides selector-based searching for Sencha Components analogous to DOM querying. The
22714 * {@link Ext.Container} class contains {@link Ext.Container#down shortcut methods} to query
22715 * its descendant Components by selector.
22716 *
22717 * Note that this id will also be used as the element id for the containing HTML element that is rendered to the
22718 * page for this component. This allows you to write id-based CSS rules to style the specific instance of this
22719 * component uniquely, and also to select sub-elements using this component's id as the parent.
22720 *
22721 * **Note**: to avoid complications imposed by a unique id also see `{@link #itemId}`.
22722 *
22723 * Defaults to an auto-assigned id.
22724 */
22725
22726 /**
22727 * @cfg {String} itemId
22728 * An itemId can be used as an alternative way to get a reference to a component when no object reference is
22729 * available. Instead of using an `{@link #id}` with {@link Ext#getCmp}, use `itemId` with
22730 * {@link Ext.Container#getComponent} which will retrieve `itemId`'s or {@link #id}'s. Since `itemId`'s are an
22731 * index to the container's internal MixedCollection, the `itemId` is scoped locally to the container - avoiding
22732 * potential conflicts with {@link Ext.ComponentManager} which requires a **unique** `{@link #id}`.
22733 *
22734 * Also see {@link #id}, {@link Ext.Container#query}, {@link Ext.Container#down} and {@link Ext.Container#child}.
22735 *
22736 * @accessor
22737 */
22738 itemId: undefined,
22739
22740 /**
22741 * @cfg {Ext.data.Model} record A model instance which updates the Component's html based on it's tpl. Similar to the data
22742 * configuration, but tied to to a record to make allow dynamic updates. This must be a model
22743 * instance and not a configuration of one.
22744 * @accessor
22745 */
22746 record: null,
22747
22748 /**
22749 * @cfg {Object/Array} plugins
22750 * @accessor
22751 * An object or array of objects that will provide custom functionality for this component. The only
22752 * requirement for a valid plugin is that it contain an init method that accepts a reference of type Ext.Component.
22753 *
22754 * When a component is created, if any plugins are available, the component will call the init method on each
22755 * plugin, passing a reference to itself. Each plugin can then call methods or respond to events on the
22756 * component as needed to provide its functionality.
22757 *
22758 * For examples of plugins, see Ext.plugin.PullRefresh and Ext.plugin.ListPaging
22759 *
22760 * ## Example code
22761 *
22762 * A plugin by alias:
22763 *
22764 * Ext.create('Ext.dataview.List', {
22765 * config: {
22766 * plugins: 'listpaging',
22767 * itemTpl: '<div class="item">{title}</div>',
22768 * store: 'Items'
22769 * }
22770 * });
22771 *
22772 * Multiple plugins by alias:
22773 *
22774 * Ext.create('Ext.dataview.List', {
22775 * config: {
22776 * plugins: ['listpaging', 'pullrefresh'],
22777 * itemTpl: '<div class="item">{title}</div>',
22778 * store: 'Items'
22779 * }
22780 * });
22781 *
22782 * Single plugin by class name with config options:
22783 *
22784 * Ext.create('Ext.dataview.List', {
22785 * config: {
22786 * plugins: {
22787 * xclass: 'Ext.plugin.ListPaging', // Reference plugin by class
22788 * autoPaging: true
22789 * },
22790 *
22791 * itemTpl: '<div class="item">{title}</div>',
22792 * store: 'Items'
22793 * }
22794 * });
22795 *
22796 * Multiple plugins by class name with config options:
22797 *
22798 * Ext.create('Ext.dataview.List', {
22799 * config: {
22800 * plugins: [
22801 * {
22802 * xclass: 'Ext.plugin.PullRefresh',
22803 * pullRefreshText: 'Pull to refresh...'
22804 * },
22805 * {
22806 * xclass: 'Ext.plugin.ListPaging',
22807 * autoPaging: true
22808 * }
22809 * ],
22810 *
22811 * itemTpl: '<div class="item">{title}</div>',
22812 * store: 'Items'
22813 * }
22814 * });
22815 *
22816 */
22817 plugins: null
22818 },
22819
22820 /**
22821 * @event show
22822 * Fires whenever the Component is shown
22823 * @param {Ext.Component} this The component instance
22824 */
22825
22826 /**
22827 * @event hide
22828 * Fires whenever the Component is hidden
22829 * @param {Ext.Component} this The component instance
22830 */
22831
22832 /**
22833 * @event fullscreen
22834 * Fires whenever a Component with the fullscreen config is instantiated
22835 * @param {Ext.Component} this The component instance
22836 */
22837
22838 /**
22839 * @event floatingchange
22840 * Fires whenever there is a change in the floating status of a component
22841 * @param {Ext.Component} this The component instance
22842 * @param {Boolean} floating The component's new floating state
22843 */
22844
22845 /**
22846 * @event destroy
22847 * Fires when the component is destroyed
22848 */
22849
22850 /**
22851 * @event beforeorientationchange
22852 * Fires before orientation changes.
22853 * @removed 2.0.0 This event is now only available `onBefore` the Viewport's {@link Ext.Viewport#orientationchange}
22854 */
22855
22856 /**
22857 * @event orientationchange
22858 * Fires when orientation changes.
22859 * @removed 2.0.0 This event is now only available on the Viewport's {@link Ext.Viewport#orientationchange}
22860 */
22861
22862 /**
22863 * @event initialize
22864 * Fires when the component has been initialized
22865 * @param {Ext.Component} this The component instance
22866 */
22867
22868 /**
22869 * @event painted
22870 * @inheritdoc Ext.dom.Element#painted
22871 * @param {Ext.Element} element The component's outer element (this.element)
22872 */
22873
22874 /**
22875 * @event erased
22876 * Fires when the component is no longer displayed in the DOM. Listening to this event will
22877 * degrade performance not recommend for general use.
22878 * @param {Ext.Component} this The component instance
22879 */
22880
22881 /**
22882 * @event resize
22883 * @inheritdoc Ext.dom.Element#resize
22884 * @param {Ext.Element} element The component's outer element (this.element)
22885 */
22886
22887 /**
22888 * @private
22889 */
22890 listenerOptionsRegex: /^(?:delegate|single|delay|buffer|args|prepend|element)$/,
22891
22892 /**
22893 * @private
22894 */
22895 alignmentRegex: /^([a-z]+)-([a-z]+)(\?)?$/,
22896
22897 /**
22898 * @private
22899 */
22900 isComponent: true,
22901
22902 /**
22903 * @private
22904 */
22905 floating: false,
22906
22907 /**
22908 * @private
22909 */
22910 rendered: false,
22911
22912 /**
22913 * @private
22914 */
22915 isInner: true,
22916
22917 /**
22918 * @private
22919 */
22920 activeAnimation: null,
22921
22922 /**
22923 * @readonly
22924 * @private
22925 */
22926 dockPositions: {
22927 top: true,
22928 right: true,
22929 bottom: true,
22930 left: true
22931 },
22932
22933 innerElement: null,
22934
22935 element: null,
22936
22937 template: [],
22938
22939 widthLayoutSized: false,
22940
22941 heightLayoutSized: false,
22942
22943 layoutStretched: false,
22944
22945 sizeState: false,
22946
22947 sizeFlags: 0x0,
22948
22949 LAYOUT_WIDTH: 0x1,
22950
22951 LAYOUT_HEIGHT: 0x2,
22952
22953 LAYOUT_BOTH: 0x3,
22954
22955 LAYOUT_STRETCHED: 0x4,
22956
22957 /**
22958 * Creates new Component.
22959 * @param {Object} config The standard configuration object.
22960 */
22961 constructor: function(config) {
22962 var me = this,
22963 currentConfig = me.config,
22964 id;
22965
22966 me.onInitializedListeners = [];
22967 me.initialConfig = config;
22968
22969 if (config !== undefined && 'id' in config) {
22970 id = config.id;
22971 }
22972 else if ('id' in currentConfig) {
22973 id = currentConfig.id;
22974 }
22975 else {
22976 id = me.getId();
22977 }
22978
22979 me.id = id;
22980 me.setId(id);
22981
22982 Ext.ComponentManager.register(me);
22983
22984 me.initElement();
22985
22986 me.initConfig(me.initialConfig);
22987
22988 me.refreshSizeState = me.doRefreshSizeState;
22989 me.refreshFloating = me.doRefreshFloating;
22990
22991 if (me.refreshSizeStateOnInitialized) {
22992 me.refreshSizeState();
22993 }
22994
22995 if (me.refreshFloatingOnInitialized) {
22996 me.refreshFloating();
22997 }
22998
22999 me.initialize();
23000
23001 me.triggerInitialized();
23002 /**
23003 * Force the component to take up 100% width and height available, by adding it to {@link Ext.Viewport}.
23004 * @cfg {Boolean} fullscreen
23005 */
23006 if (me.config.fullscreen) {
23007 me.fireEvent('fullscreen', me);
23008 }
23009
23010 me.fireEvent('initialize', me);
23011 },
23012
23013 beforeInitConfig: function(config) {
23014 this.beforeInitialize.apply(this, arguments);
23015 },
23016
23017 /**
23018 * @private
23019 */
23020 beforeInitialize: Ext.emptyFn,
23021
23022 /**
23023 * Allows addition of behavior to the rendering phase.
23024 * @protected
23025 * @template
23026 */
23027 initialize: Ext.emptyFn,
23028
23029 getTemplate: function() {
23030 return this.template;
23031 },
23032
23033 /**
23034 * @private
23035 * @return {Object}
23036 * @return {String} return.reference
23037 * @return {Array} return.classList
23038 * @return {Object} return.children
23039 */
23040 getElementConfig: function() {
23041 return {
23042 reference: 'element',
23043 classList: ['x-unsized'],
23044 children: this.getTemplate()
23045 };
23046 },
23047
23048 /**
23049 * @private
23050 */
23051 triggerInitialized: function() {
23052 var listeners = this.onInitializedListeners,
23053 ln = listeners.length,
23054 listener, fn, scope, args, i;
23055
23056 if (!this.initialized) {
23057 this.initialized = true;
23058
23059 if (ln > 0) {
23060 for (i = 0; i < ln; i++) {
23061 listener = listeners[i];
23062 fn = listener.fn;
23063 scope = listener.scope;
23064 args = listener.args;
23065
23066 if (typeof fn == 'string') {
23067 scope[fn].apply(scope, args);
23068 }
23069 else {
23070 fn.apply(scope, args);
23071 }
23072 }
23073
23074 listeners.length = 0;
23075 }
23076 }
23077 },
23078
23079 /**
23080 * @private
23081 */
23082 onInitialized: function(fn, scope, args) {
23083 var listeners = this.onInitializedListeners;
23084
23085 if (!scope) {
23086 scope = this;
23087 }
23088
23089 if (this.initialized) {
23090 if (typeof fn == 'string') {
23091 scope[fn].apply(scope, args);
23092 }
23093 else {
23094 fn.apply(scope, args);
23095 }
23096 }
23097 else {
23098 listeners.push({
23099 fn: fn,
23100 scope: scope,
23101 args: args
23102 });
23103 }
23104 },
23105
23106 renderTo: function(container, insertBeforeElement) {
23107 var dom = this.renderElement.dom,
23108 containerDom = Ext.getDom(container),
23109 insertBeforeChildDom = Ext.getDom(insertBeforeElement);
23110
23111 if (containerDom) {
23112 if (insertBeforeChildDom) {
23113 containerDom.insertBefore(dom, insertBeforeChildDom);
23114 }
23115 else {
23116 containerDom.appendChild(dom);
23117 }
23118
23119 this.setRendered(Boolean(dom.offsetParent));
23120 }
23121 },
23122
23123 /**
23124 * @private
23125 * @chainable
23126 */
23127 setParent: function(parent) {
23128 var currentParent = this.parent;
23129
23130 if (parent && currentParent && currentParent !== parent) {
23131 currentParent.remove(this, false);
23132 }
23133
23134 this.parent = parent;
23135
23136 return this;
23137 },
23138
23139 applyPlugins: function(config) {
23140 var ln, i, configObj;
23141
23142 if (!config) {
23143 return config;
23144 }
23145
23146 config = [].concat(config);
23147
23148 for (i = 0, ln = config.length; i < ln; i++) {
23149 configObj = config[i];
23150 config[i] = Ext.factory(configObj, 'Ext.plugin.Plugin', null, 'plugin');
23151 }
23152
23153 return config;
23154 },
23155
23156 updatePlugins: function(newPlugins, oldPlugins) {
23157 var ln, i;
23158
23159 if (newPlugins) {
23160 for (i = 0, ln = newPlugins.length; i < ln; i++) {
23161 newPlugins[i].init(this);
23162 }
23163 }
23164
23165 if (oldPlugins) {
23166 for (i = 0, ln = oldPlugins.length; i < ln; i++) {
23167 Ext.destroy(oldPlugins[i]);
23168 }
23169 }
23170 },
23171
23172 updateRenderTo: function(newContainer) {
23173 this.renderTo(newContainer);
23174 },
23175
23176 updateStyle: function(style) {
23177 this.element.applyStyles(style);
23178 },
23179
23180 updateBorder: function(border) {
23181 this.element.setBorder(border);
23182 },
23183
23184 updatePadding: function(padding) {
23185 this.innerElement.setPadding(padding);
23186 },
23187
23188 updateMargin: function(margin) {
23189 this.element.setMargin(margin);
23190 },
23191
23192 updateUi: function(newUi, oldUi) {
23193 var baseCls = this.getBaseCls(),
23194 element = this.element,
23195 currentUi = this.currentUi;
23196
23197 if (baseCls) {
23198 if (oldUi) {
23199 if (currentUi) {
23200 element.removeCls(currentUi);
23201 }
23202 else {
23203 element.removeCls(baseCls + '-' + oldUi);
23204 }
23205 }
23206
23207 if (newUi) {
23208 element.addCls(newUi, baseCls);
23209 this.currentUi = baseCls + '-' + newUi;
23210
23211 // The first instance gets stored on the proptotype
23212 if (!this.self.prototype.currentUi) {
23213 this.self.prototype.currentUi = this.currentUi;
23214 }
23215 }
23216 }
23217 },
23218
23219 applyBaseCls: function(baseCls) {
23220 return baseCls || clsPrefix + this.xtype;
23221 },
23222
23223 updateBaseCls: function(newBaseCls, oldBaseCls) {
23224 var me = this,
23225 ui = me.getUi();
23226
23227
23228 if (oldBaseCls) {
23229 this.element.removeCls(oldBaseCls);
23230
23231 if (ui) {
23232 this.element.removeCls(this.currentUi);
23233 }
23234 }
23235
23236 if (newBaseCls) {
23237 this.element.addCls(newBaseCls);
23238
23239 if (ui) {
23240 this.element.addCls(newBaseCls, null, ui);
23241 this.currentUi = newBaseCls + '-' + ui;
23242 }
23243 }
23244 },
23245
23246 /**
23247 * Adds a CSS class (or classes) to this Component's rendered element.
23248 * @param {String} cls The CSS class to add.
23249 * @param {String} [prefix=""] Optional prefix to add to each class.
23250 * @param {String} [suffix=""] Optional suffix to add to each class.
23251 */
23252 addCls: function(cls, prefix, suffix) {
23253 var oldCls = this.getCls(),
23254 newCls = (oldCls) ? oldCls.slice() : [],
23255 ln, i, cachedCls;
23256
23257 prefix = prefix || '';
23258 suffix = suffix || '';
23259
23260 if (typeof cls == "string") {
23261 cls = [cls];
23262 }
23263
23264 ln = cls.length;
23265
23266 //check if there is currently nothing in the array and we don't need to add a prefix or a suffix.
23267 //if true, we can just set the newCls value to the cls property, because that is what the value will be
23268 //if false, we need to loop through each and add them to the newCls array
23269 if (!newCls.length && prefix === '' && suffix === '') {
23270 newCls = cls;
23271 } else {
23272 for (i = 0; i < ln; i++) {
23273 cachedCls = prefix + cls[i] + suffix;
23274 if (newCls.indexOf(cachedCls) == -1) {
23275 newCls.push(cachedCls);
23276 }
23277 }
23278 }
23279
23280 this.setCls(newCls);
23281 },
23282
23283 /**
23284 * Removes the given CSS class(es) from this Component's rendered element.
23285 * @param {String} cls The class(es) to remove.
23286 * @param {String} [prefix=""] Optional prefix to prepend before each class.
23287 * @param {String} [suffix=""] Optional suffix to append to each class.
23288 */
23289 removeCls: function(cls, prefix, suffix) {
23290 var oldCls = this.getCls(),
23291 newCls = (oldCls) ? oldCls.slice() : [],
23292 ln, i;
23293
23294 prefix = prefix || '';
23295 suffix = suffix || '';
23296
23297 if (typeof cls == "string") {
23298 newCls = Ext.Array.remove(newCls, prefix + cls + suffix);
23299 } else {
23300 ln = cls.length;
23301 for (i = 0; i < ln; i++) {
23302 newCls = Ext.Array.remove(newCls, prefix + cls[i] + suffix);
23303 }
23304 }
23305
23306 this.setCls(newCls);
23307 },
23308
23309 /**
23310 * Replaces specified classes with the newly specified classes.
23311 * It uses the {@link #addCls} and {@link #removeCls} methods, so if the class(es) you are removing don't exist, it will
23312 * still add the new classes.
23313 * @param {String} oldCls The class(es) to remove.
23314 * @param {String} newCls The class(es) to add.
23315 * @param {String} [prefix=""] Optional prefix to prepend before each class.
23316 * @param {String} [suffix=""] Optional suffix to append to each class.
23317 */
23318 replaceCls: function(oldCls, newCls, prefix, suffix) {
23319 // We could have just called {@link #removeCls} and {@link #addCls}, but that would mean {@link #updateCls}
23320 // would get called twice, which would have performance implications because it will update the dom.
23321
23322 var cls = this.getCls(),
23323 array = (cls) ? cls.slice() : [],
23324 ln, i, cachedCls;
23325
23326 prefix = prefix || '';
23327 suffix = suffix || '';
23328
23329 //remove all oldCls
23330 if (typeof oldCls == "string") {
23331 array = Ext.Array.remove(array, prefix + oldCls + suffix);
23332 } else if (oldCls) {
23333 ln = oldCls.length;
23334 for (i = 0; i < ln; i++) {
23335 array = Ext.Array.remove(array, prefix + oldCls[i] + suffix);
23336 }
23337 }
23338
23339 //add all newCls
23340 if (typeof newCls == "string") {
23341 array.push(prefix + newCls + suffix);
23342 } else if (newCls) {
23343 ln = newCls.length;
23344
23345 //check if there is currently nothing in the array and we don't need to add a prefix or a suffix.
23346 //if true, we can just set the array value to the newCls property, because that is what the value will be
23347 //if false, we need to loop through each and add them to the array
23348 if (!array.length && prefix === '' && suffix === '') {
23349 array = newCls;
23350 } else {
23351 for (i = 0; i < ln; i++) {
23352 cachedCls = prefix + newCls[i] + suffix;
23353 if (array.indexOf(cachedCls) == -1) {
23354 array.push(cachedCls);
23355 }
23356 }
23357 }
23358 }
23359
23360 this.setCls(array);
23361 },
23362
23363 /**
23364 * Add or removes a class based on if the class is already added to the Component.
23365 *
23366 * @param {String} className The class to toggle.
23367 * @chainable
23368 */
23369 toggleCls: function(className, /* private */ force) {
23370 var oldCls = this.getCls(),
23371 newCls = (oldCls) ? oldCls.slice() : [];
23372
23373 if (force || newCls.indexOf(className) == -1) {
23374 newCls.push(className);
23375 } else {
23376 Ext.Array.remove(newCls, className);
23377 }
23378
23379 this.setCls(newCls);
23380
23381 return this;
23382 },
23383
23384 /**
23385 * @private
23386 * Checks if the `cls` is a string. If it is, changed it into an array.
23387 * @param {String/Array} cls
23388 * @return {Array/null}
23389 */
23390 applyCls: function(cls) {
23391 if (typeof cls == "string") {
23392 cls = [cls];
23393 }
23394
23395 //reset it back to null if there is nothing.
23396 if (!cls || !cls.length) {
23397 cls = null;
23398 }
23399
23400 return cls;
23401 },
23402
23403 /**
23404 * @private
23405 * All cls methods directly report to the {@link #cls} configuration, so anytime it changes, {@link #updateCls} will be called
23406 */
23407 updateCls: function (newCls, oldCls) {
23408 if (this.element && ((newCls && !oldCls) || (!newCls && oldCls) || newCls.length != oldCls.length || Ext.Array.difference(newCls,
23409 oldCls).length > 0)) {
23410 this.element.replaceCls(oldCls, newCls);
23411 }
23412 },
23413
23414 /**
23415 * Updates the {@link #styleHtmlCls} configuration
23416 */
23417 updateStyleHtmlCls: function(newHtmlCls, oldHtmlCls) {
23418 var innerHtmlElement = this.innerHtmlElement,
23419 innerElement = this.innerElement;
23420
23421 if (this.getStyleHtmlContent() && oldHtmlCls) {
23422 if (innerHtmlElement) {
23423 innerHtmlElement.replaceCls(oldHtmlCls, newHtmlCls);
23424 } else {
23425 innerElement.replaceCls(oldHtmlCls, newHtmlCls);
23426 }
23427 }
23428 },
23429
23430 applyStyleHtmlContent: function(config) {
23431 return Boolean(config);
23432 },
23433
23434 updateStyleHtmlContent: function(styleHtmlContent) {
23435 var htmlCls = this.getStyleHtmlCls(),
23436 innerElement = this.innerElement,
23437 innerHtmlElement = this.innerHtmlElement;
23438
23439 if (styleHtmlContent) {
23440 if (innerHtmlElement) {
23441 innerHtmlElement.addCls(htmlCls);
23442 } else {
23443 innerElement.addCls(htmlCls);
23444 }
23445 } else {
23446 if (innerHtmlElement) {
23447 innerHtmlElement.removeCls(htmlCls);
23448 } else {
23449 innerElement.addCls(htmlCls);
23450 }
23451 }
23452 },
23453
23454 applyContentEl: function(contentEl) {
23455 if (contentEl) {
23456 return Ext.get(contentEl);
23457 }
23458 },
23459
23460 updateContentEl: function(newContentEl, oldContentEl) {
23461 if (oldContentEl) {
23462 oldContentEl.hide();
23463 Ext.getBody().append(oldContentEl);
23464 }
23465
23466 if (newContentEl) {
23467 this.setHtml(newContentEl.dom);
23468 newContentEl.show();
23469 }
23470 },
23471
23472 /**
23473 * Returns the height and width of the Component.
23474 * @return {Object} The current `height` and `width` of the Component.
23475 * @return {Number} return.width
23476 * @return {Number} return.height
23477 */
23478 getSize: function() {
23479 return {
23480 width: this.getWidth(),
23481 height: this.getHeight()
23482 };
23483 },
23484
23485 /**
23486 * @private
23487 * @return {Boolean}
23488 */
23489 isCentered: function() {
23490 return Boolean(this.getCentered());
23491 },
23492
23493 isFloating: function() {
23494 return this.floating;
23495 },
23496
23497 isDocked: function() {
23498 return Boolean(this.getDocked());
23499 },
23500
23501 isInnerItem: function() {
23502 return this.isInner;
23503 },
23504
23505 setIsInner: function(isInner) {
23506 if (isInner !== this.isInner) {
23507 this.isInner = isInner;
23508
23509 if (this.initialized) {
23510 this.fireEvent('innerstatechange', this, isInner);
23511 }
23512 }
23513 },
23514
23515 filterLengthValue: function(value) {
23516 if (value === 'auto' || (!value && value !== 0)) {
23517 return null;
23518 }
23519
23520 return value;
23521 },
23522
23523 applyTop: function(top) {
23524 return this.filterLengthValue(top);
23525 },
23526
23527 applyRight: function(right) {
23528 return this.filterLengthValue(right);
23529 },
23530
23531 applyBottom: function(bottom) {
23532 return this.filterLengthValue(bottom);
23533 },
23534
23535 applyLeft: function(left) {
23536 return this.filterLengthValue(left);
23537 },
23538
23539 applyWidth: function(width) {
23540 return this.filterLengthValue(width);
23541 },
23542
23543 applyHeight: function(height) {
23544 return this.filterLengthValue(height);
23545 },
23546
23547 applyMinWidth: function(width) {
23548 return this.filterLengthValue(width);
23549 },
23550
23551 applyMinHeight: function(height) {
23552 return this.filterLengthValue(height);
23553 },
23554
23555 applyMaxWidth: function(width) {
23556 return this.filterLengthValue(width);
23557 },
23558
23559 applyMaxHeight: function(height) {
23560 return this.filterLengthValue(height);
23561 },
23562
23563 doSetTop: function(top) {
23564 this.element.setTop(top);
23565 this.refreshFloating();
23566 },
23567
23568 doSetRight: function(right) {
23569 this.element.setRight(right);
23570 this.refreshFloating();
23571 },
23572
23573 doSetBottom: function(bottom) {
23574 this.element.setBottom(bottom);
23575 this.refreshFloating();
23576 },
23577
23578 doSetLeft: function(left) {
23579 this.element.setLeft(left);
23580 this.refreshFloating();
23581 },
23582
23583 doSetWidth: function(width) {
23584 this.element.setWidth(width);
23585 this.refreshSizeState();
23586 },
23587
23588 doSetHeight: function(height) {
23589 this.element.setHeight(height);
23590 this.refreshSizeState();
23591 },
23592
23593 applyFlex: function(flex) {
23594 if (flex) {
23595 flex = Number(flex);
23596
23597 if (isNaN(flex)) {
23598 flex = null;
23599 }
23600 }
23601 else {
23602 flex = null
23603 }
23604
23605 return flex;
23606 },
23607
23608 doSetFlex: Ext.emptyFn,
23609
23610 refreshSizeState: function() {
23611 this.refreshSizeStateOnInitialized = true;
23612 },
23613
23614 doRefreshSizeState: function() {
23615 var hasWidth = this.getWidth() !== null || this.widthLayoutSized || (this.getLeft() !== null && this.getRight() !== null),
23616 hasHeight = this.getHeight() !== null || this.heightLayoutSized || (this.getTop() !== null && this.getBottom() !== null),
23617 stretched = this.layoutStretched || this.hasCSSMinHeight || (!hasHeight && this.getMinHeight() !== null),
23618 state = hasWidth && hasHeight,
23619 flags = (hasWidth && this.LAYOUT_WIDTH) | (hasHeight && this.LAYOUT_HEIGHT) | (stretched && this.LAYOUT_STRETCHED);
23620
23621 if (!state && stretched) {
23622 state = null;
23623 }
23624
23625 this.setSizeState(state);
23626 this.setSizeFlags(flags);
23627 },
23628
23629 setLayoutSizeFlags: function(flags) {
23630 this.layoutStretched = !!(flags & this.LAYOUT_STRETCHED);
23631 this.widthLayoutSized = !!(flags & this.LAYOUT_WIDTH);
23632 this.heightLayoutSized = !!(flags & this.LAYOUT_HEIGHT);
23633
23634 this.refreshSizeState();
23635 },
23636
23637 setSizeFlags: function(flags) {
23638 if (flags !== this.sizeFlags) {
23639 this.sizeFlags = flags;
23640
23641 var hasWidth = !!(flags & this.LAYOUT_WIDTH),
23642 hasHeight = !!(flags & this.LAYOUT_HEIGHT),
23643 stretched = !!(flags & this.LAYOUT_STRETCHED);
23644
23645 if (hasWidth && !stretched && !hasHeight) {
23646 this.element.addCls('x-has-width');
23647 }
23648 else {
23649 this.element.removeCls('x-has-width');
23650 }
23651
23652 if (hasHeight && !stretched && !hasWidth) {
23653 this.element.addCls('x-has-height');
23654 }
23655 else {
23656 this.element.removeCls('x-has-height');
23657 }
23658
23659 if (this.initialized) {
23660 this.fireEvent('sizeflagschange', this, flags);
23661 }
23662 }
23663 },
23664
23665 getSizeFlags: function() {
23666 if (!this.initialized) {
23667 this.doRefreshSizeState();
23668 }
23669
23670 return this.sizeFlags;
23671 },
23672
23673 setSizeState: function(state) {
23674 if (state !== this.sizeState) {
23675 this.sizeState = state;
23676
23677 this.element.setSizeState(state);
23678
23679 if (this.initialized) {
23680 this.fireEvent('sizestatechange', this, state);
23681 }
23682 }
23683 },
23684
23685 getSizeState: function() {
23686 if (!this.initialized) {
23687 this.doRefreshSizeState();
23688 }
23689
23690 return this.sizeState;
23691 },
23692
23693
23694 doSetMinWidth: function(width) {
23695 this.element.setMinWidth(width);
23696 },
23697
23698 doSetMinHeight: function(height) {
23699 this.element.setMinHeight(height);
23700 this.refreshSizeState();
23701 },
23702
23703 doSetMaxWidth: function(width) {
23704 this.element.setMaxWidth(width);
23705 },
23706
23707 doSetMaxHeight: function(height) {
23708 this.element.setMaxHeight(height);
23709 },
23710
23711 /**
23712 * @private
23713 * @param {Boolean} centered
23714 * @return {Boolean}
23715 */
23716 applyCentered: function(centered) {
23717 centered = Boolean(centered);
23718
23719 if (centered) {
23720 this.refreshInnerState = Ext.emptyFn;
23721
23722 if (this.isFloating()) {
23723 this.resetFloating();
23724 }
23725
23726 if (this.isDocked()) {
23727 this.setDocked(false);
23728 }
23729
23730 this.setIsInner(false);
23731 delete this.refreshInnerState;
23732 }
23733
23734 return centered;
23735 },
23736
23737 doSetCentered: function(centered) {
23738 this.toggleCls(this.getFloatingCls(), centered);
23739
23740 if (!centered) {
23741 this.refreshInnerState();
23742 }
23743 },
23744
23745 applyDocked: function(docked) {
23746 if (!docked) {
23747 return null;
23748 }
23749
23750 //<debug error>
23751 if (!/^(top|right|bottom|left)$/.test(docked)) {
23752 Ext.Logger.error("Invalid docking position of '" + docked.position + "', must be either 'top', 'right', 'bottom', " +
23753 "'left' or `null` (for no docking)", this);
23754 return;
23755 }
23756 //</debug>
23757
23758 this.refreshInnerState = Ext.emptyFn;
23759
23760 if (this.isFloating()) {
23761 this.resetFloating();
23762 }
23763
23764 if (this.isCentered()) {
23765 this.setCentered(false);
23766 }
23767
23768 this.setIsInner(false);
23769
23770 delete this.refreshInnerState;
23771
23772 return docked;
23773 },
23774
23775 doSetDocked: function(docked, oldDocked) {
23776 this.fireEvent('afterdockedchange', this, docked, oldDocked);
23777 if (!docked) {
23778 this.refreshInnerState();
23779 }
23780 },
23781
23782 /**
23783 * Resets {@link #top}, {@link #right}, {@link #bottom} and {@link #left} configurations to `null`, which
23784 * will un-float this component.
23785 */
23786 resetFloating: function() {
23787 this.setTop(null);
23788 this.setRight(null);
23789 this.setBottom(null);
23790 this.setLeft(null);
23791 },
23792
23793
23794 refreshInnerState: function() {
23795 this.setIsInner(!this.isCentered() && !this.isFloating() && !this.isDocked());
23796 },
23797
23798 refreshFloating: function() {
23799 this.refreshFloatingOnInitialized = true;
23800 },
23801
23802 doRefreshFloating: function() {
23803 var floating = true,
23804 floatingCls = this.getFloatingCls();
23805
23806 if (this.getTop() === null && this.getBottom() === null &&
23807 this.getRight() === null && this.getLeft() === null) {
23808 floating = false;
23809 }
23810 else {
23811 this.refreshSizeState();
23812 }
23813
23814 if (floating !== this.floating) {
23815 this.floating = floating;
23816
23817 if (floating) {
23818 this.refreshInnerState = Ext.emptyFn;
23819
23820 if (this.isCentered()) {
23821 this.setCentered(false);
23822 }
23823
23824 if (this.isDocked()) {
23825 this.setDocked(false);
23826 }
23827
23828 this.setIsInner(false);
23829
23830 delete this.refreshInnerState;
23831 }
23832
23833 this.element.toggleCls(floatingCls, floating);
23834
23835 if (this.initialized) {
23836 this.fireEvent('floatingchange', this, floating);
23837 }
23838
23839 if (!floating) {
23840 this.refreshInnerState();
23841 }
23842 }
23843 },
23844
23845 /**
23846 * Updates the floatingCls if the component is currently floating
23847 * @private
23848 */
23849 updateFloatingCls: function(newFloatingCls, oldFloatingCls) {
23850 if (this.isFloating()) {
23851 this.replaceCls(oldFloatingCls, newFloatingCls);
23852 }
23853 },
23854
23855 applyDisabled: function(disabled) {
23856 return Boolean(disabled);
23857 },
23858
23859 doSetDisabled: function(disabled) {
23860 this.element[disabled ? 'addCls' : 'removeCls'](this.getDisabledCls());
23861 },
23862
23863 updateDisabledCls: function(newDisabledCls, oldDisabledCls) {
23864 if (this.isDisabled()) {
23865 this.element.replaceCls(oldDisabledCls, newDisabledCls);
23866 }
23867 },
23868
23869 /**
23870 * Disables this Component
23871 */
23872 disable: function() {
23873 this.setDisabled(true);
23874 },
23875
23876 /**
23877 * Enables this Component
23878 */
23879 enable: function() {
23880 this.setDisabled(false);
23881 },
23882
23883 /**
23884 * Returns `true` if this Component is currently disabled.
23885 * @return {Boolean} `true` if currently disabled.
23886 */
23887 isDisabled: function() {
23888 return this.getDisabled();
23889 },
23890
23891 applyZIndex: function(zIndex) {
23892 if (!zIndex && zIndex !== 0) {
23893 zIndex = null;
23894 }
23895
23896 if (zIndex !== null) {
23897 zIndex = Number(zIndex);
23898
23899 if (isNaN(zIndex)) {
23900 zIndex = null;
23901 }
23902 }
23903
23904 return zIndex;
23905 },
23906
23907 updateZIndex: function(zIndex) {
23908 var element = this.element,
23909 domStyle;
23910
23911 if (element && !element.isDestroyed) {
23912 domStyle = element.dom.style;
23913 if (zIndex !== null) {
23914 domStyle.setProperty('z-index', zIndex, 'important');
23915 }
23916 else {
23917 domStyle.removeProperty('z-index');
23918 }
23919 }
23920 },
23921
23922 getInnerHtmlElement: function() {
23923 var innerHtmlElement = this.innerHtmlElement,
23924 styleHtmlCls;
23925
23926 if (!innerHtmlElement || !innerHtmlElement.dom || !innerHtmlElement.dom.parentNode) {
23927 this.innerHtmlElement = innerHtmlElement = Ext.Element.create({ cls: 'x-innerhtml' });
23928
23929 if (this.getStyleHtmlContent()) {
23930 styleHtmlCls = this.getStyleHtmlCls();
23931 this.innerHtmlElement.addCls(styleHtmlCls);
23932 this.innerElement.removeCls(styleHtmlCls);
23933 }
23934 this.innerElement.appendChild(innerHtmlElement);
23935 }
23936
23937 return innerHtmlElement;
23938 },
23939
23940 updateHtml: function(html) {
23941 if (!this.isDestroyed) {
23942 var innerHtmlElement = this.getInnerHtmlElement();
23943
23944 if (Ext.isElement(html)){
23945 innerHtmlElement.setHtml('');
23946 innerHtmlElement.append(html);
23947 }
23948 else {
23949 innerHtmlElement.setHtml(html);
23950 }
23951 }
23952 },
23953
23954 applyHidden: function(hidden) {
23955 return Boolean(hidden);
23956 },
23957
23958 doSetHidden: function(hidden) {
23959 var element = this.renderElement;
23960
23961 if (element.isDestroyed) {
23962 return;
23963 }
23964
23965 if (hidden) {
23966 element.hide();
23967 }
23968 else {
23969 element.show();
23970 }
23971
23972 if (this.element) {
23973 this.element[hidden ? 'addCls' : 'removeCls'](this.getHiddenCls());
23974 }
23975
23976 this.fireEvent(hidden ? 'hide' : 'show', this);
23977 },
23978
23979 updateHiddenCls: function(newHiddenCls, oldHiddenCls) {
23980 if (this.isHidden()) {
23981 this.element.replaceCls(oldHiddenCls, newHiddenCls);
23982 }
23983 },
23984
23985 /**
23986 * Returns `true` if this Component is currently hidden.
23987 * @return {Boolean} `true` if currently hidden.
23988 */
23989 isHidden: function() {
23990 return this.getHidden();
23991 },
23992
23993 /**
23994 * Hides this Component optionally using an animation.
23995 * @param {Object/Boolean} [animation] You can specify an animation here or a bool to use the {@link #hideAnimation} config.
23996 * @return {Ext.Component}
23997 * @chainable
23998 */
23999 hide: function(animation) {
24000 this.setCurrentAlignmentInfo(null);
24001 if(this.activeAnimation) {
24002 this.activeAnimation.on({
24003 animationend: function(){
24004 this.hide(animation);
24005 },
24006 scope: this,
24007 single: true
24008 });
24009 return this;
24010 }
24011
24012 if (!this.getHidden()) {
24013 if (animation === undefined || (animation && animation.isComponent)) {
24014 animation = this.getHideAnimation();
24015 }
24016 if (animation) {
24017 if (animation === true) {
24018 animation = 'fadeOut';
24019 }
24020 this.onBefore({
24021 hiddenchange: 'animateFn',
24022 scope: this,
24023 single: true,
24024 args: [animation]
24025 });
24026 }
24027 this.setHidden(true);
24028 }
24029 return this;
24030 },
24031
24032 /**
24033 * Shows this component optionally using an animation.
24034 * @param {Object/Boolean} [animation] You can specify an animation here or a bool to use the {@link #showAnimation} config.
24035 * @return {Ext.Component}
24036 * @chainable
24037 */
24038 show: function(animation) {
24039 if(this.activeAnimation) {
24040 this.activeAnimation.on({
24041 animationend: function(){
24042 this.show(animation);
24043 },
24044 scope: this,
24045 single: true
24046 });
24047 return this;
24048 }
24049
24050 var hidden = this.getHidden();
24051 if (hidden || hidden === null) {
24052 if (animation === true) {
24053 animation = 'fadeIn';
24054 }
24055 else if (animation === undefined || (animation && animation.isComponent)) {
24056 animation = this.getShowAnimation();
24057 }
24058
24059 if (animation) {
24060 this.beforeShowAnimation();
24061 this.onBefore({
24062 hiddenchange: 'animateFn',
24063 scope: this,
24064 single: true,
24065 args: [animation]
24066 });
24067 }
24068
24069 this.setHidden(false);
24070 }
24071
24072 return this;
24073 },
24074
24075 beforeShowAnimation: function() {
24076 if (this.element) {
24077 this.renderElement.show();
24078 this.element.removeCls(this.getHiddenCls());
24079 }
24080 },
24081
24082 animateFn: function(animation, component, newState, oldState, options, controller) {
24083 var me = this;
24084 if (animation && (!newState || (newState && this.isPainted()))) {
24085
24086 this.activeAnimation = new Ext.fx.Animation(animation);
24087 this.activeAnimation.setElement(component.element);
24088
24089 if (!Ext.isEmpty(newState)) {
24090 this.activeAnimation.setOnEnd(function() {
24091 me.activeAnimation = null;
24092 controller.resume();
24093 });
24094
24095 controller.pause();
24096 }
24097
24098 Ext.Animator.run(me.activeAnimation);
24099 }
24100 },
24101
24102 /**
24103 * @private
24104 */
24105 setVisibility: function(isVisible) {
24106 this.renderElement.setVisibility(isVisible);
24107 },
24108
24109 /**
24110 * @private
24111 */
24112 isRendered: function() {
24113 return this.rendered;
24114 },
24115
24116 /**
24117 * @private
24118 */
24119 isPainted: function() {
24120 return this.renderElement.isPainted();
24121 },
24122
24123 /**
24124 * @private
24125 */
24126 applyTpl: function(config) {
24127 return (Ext.isObject(config) && config.isTemplate) ? config : new Ext.XTemplate(config);
24128 },
24129
24130 applyData: function(data) {
24131 if (Ext.isObject(data)) {
24132 return Ext.apply({}, data);
24133 } else if (!data) {
24134 data = {};
24135 }
24136
24137 return data;
24138 },
24139
24140 /**
24141 * @private
24142 */
24143 updateData: function(newData) {
24144 var me = this;
24145 if (newData) {
24146 var tpl = me.getTpl(),
24147 tplWriteMode = me.getTplWriteMode();
24148
24149 if (tpl) {
24150 tpl[tplWriteMode](me.getInnerHtmlElement(), newData);
24151 }
24152
24153 /**
24154 * @event updatedata
24155 * Fires whenever the data of the component is updated
24156 * @param {Ext.Component} this The component instance
24157 * @param {Object} newData The new data
24158 */
24159 this.fireEvent('updatedata', me, newData);
24160 }
24161 },
24162
24163 applyRecord: function(config) {
24164 if (config && Ext.isObject(config) && config.isModel) {
24165 return config;
24166 }
24167 return null;
24168 },
24169
24170 updateRecord: function(newRecord, oldRecord) {
24171 var me = this;
24172
24173 if (oldRecord) {
24174 oldRecord.unjoin(me);
24175 }
24176
24177 if (!newRecord) {
24178 me.updateData('');
24179 }
24180 else {
24181 newRecord.join(me);
24182 me.updateData(newRecord.getData(true));
24183 }
24184 },
24185
24186 // @private Used to handle joining of a record to a tpl
24187 afterEdit: function() {
24188 this.updateRecord(this.getRecord());
24189 },
24190
24191 // @private Used to handle joining of a record to a tpl
24192 afterErase: function() {
24193 this.setRecord(null);
24194 },
24195
24196 applyItemId: function(itemId) {
24197 return itemId || this.getId();
24198 },
24199
24200 /**
24201 * Tests whether or not this Component is of a specific xtype. This can test whether this Component is descended
24202 * from the xtype (default) or whether it is directly of the xtype specified (`shallow = true`).
24203 * __If using your own subclasses, be aware that a Component must register its own xtype
24204 * to participate in determination of inherited xtypes.__
24205 *
24206 * For a list of all available xtypes, see the {@link Ext.Component} header.
24207 *
24208 * Example usage:
24209 *
24210 * var t = new Ext.field.Text();
24211 * var isText = t.isXType('textfield'); // true
24212 * var isBoxSubclass = t.isXType('field'); // true, descended from Ext.field.Field
24213 * var isBoxInstance = t.isXType('field', true); // false, not a direct Ext.field.Field instance
24214 *
24215 * @param {String} xtype The xtype to check for this Component.
24216 * @param {Boolean} shallow (optional) `false` to check whether this Component is descended from the xtype (this is
24217 * the default), or `true` to check whether this Component is directly of the specified xtype.
24218 * @return {Boolean} `true` if this component descends from the specified xtype, `false` otherwise.
24219 */
24220 isXType: function(xtype, shallow) {
24221 if (shallow) {
24222 return this.xtypes.indexOf(xtype) != -1;
24223 }
24224
24225 return Boolean(this.xtypesMap[xtype]);
24226 },
24227
24228 /**
24229 * Returns this Component's xtype hierarchy as a slash-delimited string. For a list of all
24230 * available xtypes, see the {@link Ext.Component} header.
24231 *
24232 * __Note:__ If using your own subclasses, be aware that a Component must register its own xtype
24233 * to participate in determination of inherited xtypes.
24234 *
24235 * Example usage:
24236 *
24237 * var t = new Ext.field.Text();
24238 * alert(t.getXTypes()); // alerts 'component/field/textfield'
24239 *
24240 * @return {String} The xtype hierarchy string.
24241 */
24242 getXTypes: function() {
24243 return this.xtypesChain.join('/');
24244 },
24245
24246 getDraggableBehavior: function() {
24247 var behavior = this.draggableBehavior;
24248
24249 if (!behavior) {
24250 behavior = this.draggableBehavior = new Ext.behavior.Draggable(this);
24251 }
24252
24253 return behavior;
24254 },
24255
24256 applyDraggable: function(config) {
24257 this.getDraggableBehavior().setConfig(config);
24258 },
24259
24260 getDraggable: function() {
24261 return this.getDraggableBehavior().getDraggable();
24262 },
24263
24264 getTranslatableBehavior: function() {
24265 var behavior = this.translatableBehavior;
24266
24267 if (!behavior) {
24268 behavior = this.translatableBehavior = new Ext.behavior.Translatable(this);
24269 }
24270
24271 return behavior;
24272 },
24273
24274 applyTranslatable: function(config) {
24275 this.getTranslatableBehavior().setConfig(config);
24276 },
24277
24278 getTranslatable: function() {
24279 return this.getTranslatableBehavior().getTranslatable();
24280 },
24281
24282 translateAxis: function(axis, value, animation) {
24283 var x, y;
24284
24285 if (axis === 'x') {
24286 x = value;
24287 }
24288 else {
24289 y = value;
24290 }
24291
24292 return this.translate(x, y, animation);
24293 },
24294
24295 translate: function() {
24296 var translatable = this.getTranslatable();
24297
24298 if (!translatable) {
24299 this.setTranslatable(true);
24300 translatable = this.getTranslatable();
24301 }
24302
24303 translatable.translate.apply(translatable, arguments);
24304 },
24305
24306 /**
24307 * @private
24308 * @param {Boolean} rendered
24309 */
24310 setRendered: function(rendered) {
24311 var wasRendered = this.rendered;
24312
24313 if (rendered !== wasRendered) {
24314 this.rendered = rendered;
24315
24316 return true;
24317 }
24318
24319 return false;
24320 },
24321
24322 /**
24323 * Sets the size of the Component.
24324 * @param {Number} width The new width for the Component.
24325 * @param {Number} height The new height for the Component.
24326 */
24327 setSize: function(width, height) {
24328 if (width != undefined) {
24329 this.setWidth(width);
24330 }
24331 if (height != undefined) {
24332 this.setHeight(height);
24333 }
24334 },
24335
24336 //@private
24337 doAddListener: function(name, fn, scope, options, order) {
24338 if (options && 'element' in options) {
24339 //<debug error>
24340 if (this.referenceList.indexOf(options.element) === -1) {
24341 Ext.Logger.error("Adding event listener with an invalid element reference of '" + options.element +
24342 "' for this component. Available values are: '" + this.referenceList.join("', '") + "'", this);
24343 }
24344 //</debug>
24345
24346 // The default scope is this component
24347 return this[options.element].doAddListener(name, fn, scope || this, options, order);
24348 }
24349 if (name == 'painted' || name == 'resize') {
24350 return this.element.doAddListener(name, fn, scope || this, options, order);
24351 }
24352
24353 return this.callParent(arguments);
24354 },
24355
24356 //@private
24357 doRemoveListener: function(name, fn, scope, options, order) {
24358 if (options && 'element' in options) {
24359 //<debug error>
24360 if (this.referenceList.indexOf(options.element) === -1) {
24361 Ext.Logger.error("Removing event listener with an invalid element reference of '" + options.element +
24362 "' for this component. Available values are: '" + this.referenceList.join('", "') + "'", this);
24363 }
24364 //</debug>
24365
24366 // The default scope is this component
24367 this[options.element].doRemoveListener(name, fn, scope || this, options, order);
24368 }
24369
24370 return this.callParent(arguments);
24371 },
24372
24373 /**
24374 * Shows this component by another component. If you specify no alignment, it will automatically
24375 * position this component relative to the reference component.
24376 *
24377 * For example, say we are aligning a Panel next to a Button, the alignment string would look like this:
24378 *
24379 * [panel-vertical (t/b/c)][panel-horizontal (l/r/c)]-[button-vertical (t/b/c)][button-horizontal (l/r/c)]
24380 *
24381 * where t = top, b = bottom, c = center, l = left, r = right.
24382 *
24383 * ## Examples
24384 *
24385 * - `tl-tr` means top-left corner of the Panel to the top-right corner of the Button
24386 * - `tc-bc` means top-center of the Panel to the bottom-center of the Button
24387 *
24388 * You can put a '?' at the end of the alignment string to constrain the floating element to the
24389 * {@link Ext.Viewport Viewport}
24390 *
24391 * // show `panel` by `button` using the default positioning (auto fit)
24392 * panel.showBy(button);
24393 *
24394 * // align the top left corner of `panel` with the top right corner of `button` (constrained to viewport)
24395 * panel.showBy(button, "tl-tr?");
24396 *
24397 * // align the bottom right corner of `panel` with the center left edge of `button` (not constrained by viewport)
24398 * panel.showBy(button, "br-cl");
24399 *
24400 * @param {Ext.Component} component The target component to show this component by.
24401 * @param {String} alignment (optional) The specific alignment.
24402 */
24403 showBy: function(component, alignment) {
24404 var me = this,
24405 viewport = Ext.Viewport,
24406 parent = me.getParent();
24407
24408 me.setVisibility(false);
24409
24410 if (parent !== viewport) {
24411 viewport.add(me);
24412 }
24413
24414 me.show();
24415
24416 me.on({
24417 hide: 'onShowByErased',
24418 destroy: 'onShowByErased',
24419 single: true,
24420 scope: me
24421 });
24422 viewport.on('resize', 'alignTo', me, { args: [component, alignment] });
24423
24424 me.alignTo(component, alignment);
24425 me.setVisibility(true);
24426 },
24427
24428 /**
24429 * @private
24430 * @param {Ext.Component} component
24431 */
24432 onShowByErased: function() {
24433 Ext.Viewport.un('resize', 'alignTo', this);
24434 },
24435
24436 /**
24437 * Prepares information on aligning this to component using alignment.
24438 * Also checks to see if this is already aligned to component according to alignment.
24439 * @protected
24440 */
24441 getAlignmentInfo: function (component, alignment){
24442 var alignToElement = component.isComponent ? component.renderElement : component,
24443 alignToBox = alignToElement.getPageBox(),
24444 element = this.renderElement,
24445 box = element.getPageBox(),
24446 stats = {
24447 alignToBox: alignToBox,
24448 alignment: alignment,
24449 top: alignToBox.top,
24450 left: alignToBox.left,
24451 alignToWidth: alignToBox.width,
24452 alignToHeight: alignToBox.height,
24453 width: box.width,
24454 height: box.height
24455 },
24456 currentAlignmentInfo = this.getCurrentAlignmentInfo(),
24457 isAligned = true;
24458
24459 if (!Ext.isEmpty(currentAlignmentInfo)) {
24460 Ext.Object.each(stats, function(key, value) {
24461 if (!Ext.isObject(value) && currentAlignmentInfo[key] != value) {
24462 isAligned = false;
24463 return false;
24464 }
24465 return true;
24466 });
24467 } else {
24468 isAligned = false;
24469 }
24470
24471 return {isAligned: isAligned, stats: stats};
24472 },
24473
24474 /**
24475 * Current Alignment information from the last alignTo call
24476 * @private
24477 */
24478 getCurrentAlignmentInfo: function() {
24479 return this.$currentAlignmentInfo;
24480 },
24481
24482 /**
24483 * Sets the current Alignment information, called by alignTo
24484 * @private
24485 */
24486 setCurrentAlignmentInfo: function(alignmentInfo) {
24487 this.$currentAlignmentInfo = Ext.isEmpty(alignmentInfo) ? null : Ext.merge({}, alignmentInfo.stats ? alignmentInfo.stats : alignmentInfo);
24488 },
24489
24490 /**
24491 * @private
24492 */
24493 alignTo: function(component, alignment) {
24494 var alignmentInfo = this.getAlignmentInfo(component, alignment);
24495 if(alignmentInfo.isAligned) return;
24496
24497 var alignToBox = alignmentInfo.stats.alignToBox,
24498 constrainBox = this.getParent().element.getPageBox(),
24499 alignToHeight = alignmentInfo.stats.alignToHeight,
24500 alignToWidth = alignmentInfo.stats.alignToWidth,
24501 height = alignmentInfo.stats.height,
24502 width = alignmentInfo.stats.width;
24503
24504 // Keep off the sides...
24505 constrainBox.bottom -= 5;
24506 constrainBox.height -= 10;
24507 constrainBox.left += 5;
24508 constrainBox.right -= 5;
24509 constrainBox.top += 5;
24510 constrainBox.width -= 10;
24511
24512 if (!alignment || alignment === 'auto') {
24513 if (constrainBox.bottom - alignToBox.bottom < height) {
24514 if (alignToBox.top - constrainBox.top < height) {
24515 if (alignToBox.left - constrainBox.left < width) {
24516 alignment = 'cl-cr?';
24517 }
24518 else {
24519 alignment = 'cr-cl?';
24520 }
24521 }
24522 else {
24523 alignment = 'bc-tc?';
24524 }
24525 }
24526 else {
24527 alignment = 'tc-bc?';
24528 }
24529 }
24530
24531 var matches = alignment.match(this.alignmentRegex);
24532 //<debug error>
24533 if (!matches) {
24534 Ext.Logger.error("Invalid alignment value of '" + alignment + "'");
24535 }
24536 //</debug>
24537
24538 var from = matches[1].split(''),
24539 to = matches[2].split(''),
24540 constrained = (matches[3] === '?'),
24541 fromVertical = from[0],
24542 fromHorizontal = from[1] || fromVertical,
24543 toVertical = to[0],
24544 toHorizontal = to[1] || toVertical,
24545 top = alignToBox.top,
24546 left = alignToBox.left,
24547 halfAlignHeight = alignToHeight / 2,
24548 halfAlignWidth = alignToWidth / 2,
24549 halfWidth = width / 2,
24550 halfHeight = height / 2,
24551 maxLeft, maxTop;
24552
24553 switch (fromVertical) {
24554 case 't':
24555 switch (toVertical) {
24556 case 'c':
24557 top += halfAlignHeight;
24558 break;
24559 case 'b':
24560 top += alignToHeight;
24561 }
24562 break;
24563
24564 case 'b':
24565 switch (toVertical) {
24566 case 'c':
24567 top -= (height - halfAlignHeight);
24568 break;
24569 case 't':
24570 top -= height;
24571 break;
24572 case 'b':
24573 top -= height - alignToHeight;
24574 }
24575 break;
24576
24577 case 'c':
24578 switch (toVertical) {
24579 case 't':
24580 top -= halfHeight;
24581 break;
24582 case 'c':
24583 top -= (halfHeight - halfAlignHeight);
24584 break;
24585 case 'b':
24586 top -= (halfHeight - alignToHeight);
24587 }
24588 break;
24589 }
24590
24591 switch (fromHorizontal) {
24592 case 'l':
24593 switch (toHorizontal) {
24594 case 'c':
24595 left += halfAlignHeight;
24596 break;
24597 case 'r':
24598 left += alignToWidth;
24599 }
24600 break;
24601
24602 case 'r':
24603 switch (toHorizontal) {
24604 case 'r':
24605 left -= (width - alignToWidth);
24606 break;
24607 case 'c':
24608 left -= (width - halfWidth);
24609 break;
24610 case 'l':
24611 left -= width;
24612 }
24613 break;
24614
24615 case 'c':
24616 switch (toHorizontal) {
24617 case 'l':
24618 left -= halfWidth;
24619 break;
24620 case 'c':
24621 left -= (halfWidth - halfAlignWidth);
24622 break;
24623 case 'r':
24624 left -= (halfWidth - alignToWidth);
24625 }
24626 break;
24627 }
24628
24629 if (constrained) {
24630 maxLeft = (constrainBox.left + constrainBox.width) - width;
24631 maxTop = (constrainBox.top + constrainBox.height) - height;
24632
24633 left = Math.max(constrainBox.left, Math.min(maxLeft, left));
24634 top = Math.max(constrainBox.top, Math.min(maxTop, top));
24635 }
24636
24637 this.setLeft(left);
24638 this.setTop(top);
24639 this.setCurrentAlignmentInfo(alignmentInfo);
24640 },
24641
24642 /**
24643 * Walks up the `ownerCt` axis looking for an ancestor Container which matches
24644 * the passed simple selector.
24645 *
24646 * Example:
24647 *
24648 * var owningTabPanel = grid.up('tabpanel');
24649 *
24650 * @param {String} selector (optional) The simple selector to test.
24651 * @return {Ext.Container} The matching ancestor Container (or `undefined` if no match was found).
24652 */
24653 up: function(selector) {
24654 var result = this.parent;
24655
24656 if (selector) {
24657 for (; result; result = result.parent) {
24658 if (Ext.ComponentQuery.is(result, selector)) {
24659 return result;
24660 }
24661 }
24662 }
24663 return result;
24664 },
24665
24666 getBubbleTarget: function() {
24667 return this.getParent();
24668 },
24669
24670 /**
24671 * Destroys this Component. If it is currently added to a Container it will first be removed from that Container.
24672 * All Ext.Element references are also deleted and the Component is de-registered from Ext.ComponentManager
24673 */
24674 destroy: function() {
24675 this.destroy = Ext.emptyFn;
24676
24677 var parent = this.getParent(),
24678 referenceList = this.referenceList,
24679 i, ln, reference;
24680
24681 this.isDestroying = true;
24682 Ext.destroy(this.getTranslatable(), this.getPlugins());
24683
24684 // Remove this component itself from the container if it's currently contained
24685 if (parent) {
24686 parent.remove(this, false);
24687 }
24688
24689 // Destroy all element references
24690 for (i = 0, ln = referenceList.length; i < ln; i++) {
24691 reference = referenceList[i];
24692 this[reference].destroy();
24693 delete this[reference];
24694 }
24695
24696 Ext.destroy(this.innerHtmlElement);
24697 this.setRecord(null);
24698
24699 this.callSuper();
24700
24701 Ext.ComponentManager.unregister(this);
24702 }
24703
24704 // Convert old properties in data into a config object
24705
24706 }, function() {
24707 });
24708
24709 })(Ext.baseCSSPrefix);
24710
24711 /**
24712 *
24713 */
24714 Ext.define('Ext.layout.wrapper.Inner', {
24715 config: {
24716 sizeState: null,
24717 container: null
24718 },
24719
24720 constructor: function(config) {
24721 this.initConfig(config);
24722 },
24723
24724 getElement: function() {
24725 return this.getContainer().bodyElement;
24726 },
24727
24728 setInnerWrapper: Ext.emptyFn,
24729
24730 getInnerWrapper: Ext.emptyFn
24731 });
24732
24733 /**
24734 *
24735 */
24736 Ext.define('Ext.layout.Abstract', {
24737 mixins: [ Ext.mixin.Observable ],
24738
24739 isLayout: true,
24740
24741 constructor: function(config) {
24742 this.initialConfig = config;
24743 },
24744
24745 setContainer: function(container) {
24746 this.container = container;
24747
24748 this.initConfig(this.initialConfig);
24749
24750 return this;
24751 },
24752
24753 onItemAdd: function() {},
24754
24755 onItemRemove: function() {},
24756
24757 onItemMove: function() {},
24758
24759 onItemCenteredChange: function() {},
24760
24761 onItemFloatingChange: function() {},
24762
24763 onItemDockedChange: function() {},
24764
24765 onItemInnerStateChange: function() {}
24766 });
24767
24768 /**
24769 *
24770 */
24771 Ext.define('Ext.mixin.Bindable', {
24772 extend: Ext.mixin.Mixin ,
24773
24774 mixinConfig: {
24775 id: 'bindable'
24776 },
24777
24778 bind: function(instance, boundMethod, bindingMethod, preventDefault, extraArgs) {
24779 if (!bindingMethod) {
24780 bindingMethod = boundMethod;
24781 }
24782
24783 var boundFn = instance[boundMethod],
24784 fn, binding;
24785
24786 if (boundFn && boundFn.hasOwnProperty('$binding')) {
24787 binding = boundFn.$binding;
24788 if (binding.bindingMethod === bindingMethod && binding.bindingScope === this) {
24789 return this;
24790 }
24791 }
24792
24793 instance[boundMethod] = fn = function() {
24794 var binding = fn.$binding,
24795 scope = binding.bindingScope,
24796 args = Array.prototype.slice.call(arguments);
24797
24798 args.push(arguments);
24799
24800 if (extraArgs) {
24801 args.push.apply(args, extraArgs);
24802 }
24803
24804 if (!binding.preventDefault && scope[binding.bindingMethod].apply(scope, args) !== false) {
24805 return binding.boundFn.apply(this, arguments);
24806 }
24807 };
24808 fn.$binding = {
24809 preventDefault: !!preventDefault,
24810 boundFn: boundFn,
24811 bindingMethod: bindingMethod,
24812 bindingScope: this
24813 };
24814
24815 return this;
24816 },
24817
24818 unbind: function(instance, boundMethod, bindingMethod) {
24819 if (!bindingMethod) {
24820 bindingMethod = boundMethod;
24821 }
24822
24823 var fn = instance[boundMethod],
24824 binding = fn.$binding,
24825 boundFn, currentBinding;
24826
24827 while (binding) {
24828 boundFn = binding.boundFn;
24829
24830 if (binding.bindingMethod === bindingMethod && binding.bindingScope === this) {
24831 if (currentBinding) {
24832 currentBinding.boundFn = boundFn;
24833 }
24834 else {
24835 instance[boundMethod] = boundFn;
24836 }
24837
24838 return this;
24839 }
24840
24841 currentBinding = binding;
24842 binding = boundFn.$binding;
24843 }
24844
24845 return this;
24846 }
24847 });
24848
24849 /**
24850 *
24851 */
24852 Ext.define('Ext.util.Wrapper', {
24853 mixins: [ Ext.mixin.Bindable ],
24854
24855 constructor: function(elementConfig, wrappedElement) {
24856 var element = this.link('element', Ext.Element.create(elementConfig));
24857
24858 if (wrappedElement) {
24859 element.insertBefore(wrappedElement);
24860 this.wrap(wrappedElement);
24861 }
24862 },
24863
24864 bindSize: function(sizeName) {
24865 var wrappedElement = this.wrappedElement,
24866 boundMethodName;
24867
24868 this.boundSizeName = sizeName;
24869 this.boundMethodName = boundMethodName = sizeName === 'width' ? 'setWidth' : 'setHeight';
24870
24871 this.bind(wrappedElement, boundMethodName, 'onBoundSizeChange');
24872 wrappedElement[boundMethodName].call(wrappedElement, wrappedElement.getStyleValue(sizeName));
24873 },
24874
24875 onBoundSizeChange: function(size, args) {
24876 var element = this.element;
24877
24878 if (typeof size === 'string' && size.substr(-1) === '%') {
24879 args[0] = '100%';
24880 }
24881 else {
24882 size = '';
24883 }
24884
24885 element[this.boundMethodName].call(element, size);
24886 },
24887
24888 wrap: function(wrappedElement) {
24889 var element = this.element,
24890 innerDom;
24891
24892 this.wrappedElement = wrappedElement;
24893
24894 innerDom = element.dom;
24895
24896 while (innerDom.firstElementChild !== null) {
24897 innerDom = innerDom.firstElementChild;
24898 }
24899
24900 innerDom.appendChild(wrappedElement.dom);
24901 },
24902
24903 destroy: function() {
24904 var element = this.element,
24905 dom = element.dom,
24906 wrappedElement = this.wrappedElement,
24907 boundMethodName = this.boundMethodName,
24908 parentNode = dom.parentNode,
24909 size;
24910
24911 if (boundMethodName) {
24912 this.unbind(wrappedElement, boundMethodName, 'onBoundSizeChange');
24913 size = element.getStyle(this.boundSizeName);
24914
24915 if (size) {
24916 wrappedElement[boundMethodName].call(wrappedElement, size);
24917 }
24918 }
24919
24920 if (parentNode) {
24921 if (!wrappedElement.isDestroyed) {
24922 parentNode.replaceChild(dom.firstElementChild, dom);
24923 }
24924 delete this.wrappedElement;
24925 }
24926
24927 this.callSuper();
24928 }
24929 });
24930
24931 /**
24932 *
24933 */
24934 Ext.define('Ext.layout.wrapper.BoxDock', {
24935 config: {
24936 direction: 'horizontal',
24937 element: {
24938 className: 'x-dock'
24939 },
24940 bodyElement: {
24941 className: 'x-dock-body'
24942 },
24943 innerWrapper: null,
24944 sizeState: false,
24945 container: null
24946 },
24947
24948 positionMap: {
24949 top: 'start',
24950 left: 'start',
24951 bottom: 'end',
24952 right: 'end'
24953 },
24954
24955 constructor: function(config) {
24956 this.items = {
24957 start: [],
24958 end: []
24959 };
24960
24961 this.itemsCount = 0;
24962
24963 this.initConfig(config);
24964 },
24965
24966 addItems: function(items) {
24967 var i, ln, item;
24968
24969 for (i = 0, ln = items.length; i < ln; i++) {
24970 item = items[i];
24971 this.addItem(item);
24972 }
24973 },
24974
24975 addItem: function(item) {
24976 var docked = item.getDocked(),
24977 position = this.positionMap[docked],
24978 wrapper = item.$dockWrapper,
24979 container = this.getContainer(),
24980 index = container.indexOf(item),
24981 element = item.element,
24982 items = this.items,
24983 sideItems = items[position],
24984 i, ln, sibling, referenceElement, siblingIndex;
24985
24986 if (wrapper) {
24987 wrapper.removeItem(item);
24988 }
24989
24990 item.$dockWrapper = this;
24991 item.addCls('x-dock-item');
24992 item.addCls('x-docked-' + docked);
24993
24994 for (i = 0, ln = sideItems.length; i < ln; i++) {
24995 sibling = sideItems[i];
24996 siblingIndex = container.indexOf(sibling);
24997
24998 if (siblingIndex > index) {
24999 referenceElement = sibling.element;
25000 sideItems.splice(i, 0, item);
25001 break;
25002 }
25003 }
25004
25005 if (!referenceElement) {
25006 sideItems.push(item);
25007 referenceElement = this.getBodyElement();
25008 }
25009
25010 this.itemsCount++;
25011
25012 if (position === 'start') {
25013 element.insertBefore(referenceElement);
25014 }
25015 else {
25016 element.insertAfter(referenceElement);
25017 }
25018 },
25019
25020 removeItem: function(item) {
25021 var position = item.getDocked(),
25022 items = this.items[this.positionMap[position]];
25023
25024 Ext.Array.remove(items, item);
25025 item.element.detach();
25026 delete item.$dockWrapper;
25027 item.removeCls('x-dock-item');
25028 item.removeCls('x-docked-' + position);
25029
25030 if (--this.itemsCount === 0) {
25031 this.destroy();
25032 }
25033 },
25034
25035 getItemsSlice: function(index) {
25036 var container = this.getContainer(),
25037 items = this.items,
25038 slice = [],
25039 sideItems, i, ln, item;
25040
25041 for (sideItems = items.start, i = 0, ln = sideItems.length; i < ln; i++) {
25042 item = sideItems[i];
25043 if (container.indexOf(item) > index) {
25044 slice.push(item);
25045 }
25046 }
25047
25048 for (sideItems = items.end, i = 0, ln = sideItems.length; i < ln; i++) {
25049 item = sideItems[i];
25050 if (container.indexOf(item) > index) {
25051 slice.push(item);
25052 }
25053 }
25054
25055 return slice;
25056 },
25057
25058 applyElement: function(element) {
25059 return Ext.Element.create(element);
25060 },
25061
25062 updateElement: function(element) {
25063 element.addCls('x-dock-' + this.getDirection());
25064 },
25065
25066 applyBodyElement: function(bodyElement) {
25067 return Ext.Element.create(bodyElement);
25068 },
25069
25070 updateBodyElement: function(bodyElement) {
25071 this.getElement().append(bodyElement);
25072 },
25073
25074 updateInnerWrapper: function(innerWrapper, oldInnerWrapper) {
25075 var bodyElement = this.getBodyElement();
25076
25077 if (oldInnerWrapper && oldInnerWrapper.$outerWrapper === this) {
25078 oldInnerWrapper.getElement().detach();
25079 delete oldInnerWrapper.$outerWrapper;
25080 }
25081
25082 if (innerWrapper) {
25083 innerWrapper.setSizeState(this.getSizeState());
25084 innerWrapper.$outerWrapper = this;
25085 bodyElement.append(innerWrapper.getElement());
25086 }
25087 },
25088
25089 updateSizeState: function(state) {
25090 var innerWrapper = this.getInnerWrapper();
25091
25092 this.getElement().setSizeState(state);
25093
25094 if (innerWrapper) {
25095 innerWrapper.setSizeState(state);
25096 }
25097 },
25098
25099 destroy: function() {
25100 var innerWrapper = this.getInnerWrapper(),
25101 outerWrapper = this.$outerWrapper,
25102 innerWrapperElement;
25103
25104 if (innerWrapper) {
25105 if (outerWrapper) {
25106 outerWrapper.setInnerWrapper(innerWrapper);
25107 }
25108 else {
25109 innerWrapperElement = innerWrapper.getElement();
25110 if (!innerWrapperElement.isDestroyed) {
25111 innerWrapperElement.replace(this.getElement());
25112 }
25113 delete innerWrapper.$outerWrapper;
25114 }
25115 }
25116
25117 delete this.$outerWrapper;
25118
25119 this.setInnerWrapper(null);
25120
25121 this.unlink('_bodyElement', '_element');
25122
25123 this.callSuper();
25124 }
25125 });
25126
25127 /**
25128 *
25129 */
25130 Ext.define('Ext.layout.Default', {
25131 extend: Ext.layout.Abstract ,
25132
25133 isAuto: true,
25134
25135 alias: ['layout.default', 'layout.auto'],
25136
25137
25138
25139
25140
25141
25142
25143 config: {
25144 /**
25145 * @cfg {Ext.fx.layout.Card} animation Layout animation configuration
25146 * Controls how layout transitions are animated. Currently only available for
25147 * Card Layouts.
25148 *
25149 * Possible values are:
25150 *
25151 * - cover
25152 * - cube
25153 * - fade
25154 * - flip
25155 * - pop
25156 * - reveal
25157 * - scroll
25158 * - slide
25159 * @accessor
25160 */
25161 animation: null
25162 },
25163
25164 centerWrapperClass: 'x-center',
25165
25166 dockWrapperClass: 'x-dock',
25167
25168 positionMap: {
25169 top: 'start',
25170 left: 'start',
25171 middle: 'center',
25172 bottom: 'end',
25173 right: 'end'
25174 },
25175
25176 positionDirectionMap: {
25177 top: 'vertical',
25178 bottom: 'vertical',
25179 left: 'horizontal',
25180 right: 'horizontal'
25181 },
25182
25183 setContainer: function(container) {
25184 var options = {
25185 delegate: '> component'
25186 };
25187
25188 this.dockedItems = [];
25189
25190 this.callSuper(arguments);
25191
25192 container.on('centeredchange', 'onItemCenteredChange', this, options, 'before')
25193 .on('floatingchange', 'onItemFloatingChange', this, options, 'before')
25194 .on('dockedchange', 'onBeforeItemDockedChange', this, options, 'before')
25195 .on('afterdockedchange', 'onAfterItemDockedChange', this, options);
25196 },
25197
25198 monitorSizeStateChange: function() {
25199 this.monitorSizeStateChange = Ext.emptyFn;
25200 this.container.on('sizestatechange', 'onContainerSizeStateChange', this);
25201 },
25202
25203 monitorSizeFlagsChange: function() {
25204 this.monitorSizeFlagsChange = Ext.emptyFn;
25205 this.container.on('sizeflagschange', 'onContainerSizeFlagsChange', this);
25206 },
25207
25208 onItemAdd: function(item) {
25209 var docked = item.getDocked();
25210
25211 if (docked !== null) {
25212 this.dockItem(item);
25213 }
25214 else if (item.isCentered()) {
25215 this.onItemCenteredChange(item, true);
25216 }
25217 else if (item.isFloating()) {
25218 this.onItemFloatingChange(item, true);
25219 }
25220 else {
25221 this.onItemInnerStateChange(item, true);
25222 }
25223 },
25224
25225 /**
25226 * @param {Ext.Component} item
25227 * @param {Boolean} isInner
25228 * @param {Boolean} [destroying]
25229 */
25230 onItemInnerStateChange: function(item, isInner, destroying) {
25231 if (isInner) {
25232 this.insertInnerItem(item, this.container.innerIndexOf(item));
25233 }
25234 else {
25235 this.removeInnerItem(item);
25236 }
25237 },
25238
25239 insertInnerItem: function(item, index) {
25240 var container = this.container,
25241 containerDom = container.innerElement.dom,
25242 itemDom = item.element.dom,
25243 nextSibling = index !== -1 ? container.getInnerAt(index + 1) : null,
25244 nextSiblingDom = null,
25245 translatable;
25246
25247 if (nextSibling) {
25248 translatable = nextSibling.getTranslatable();
25249 if (translatable && translatable.getUseWrapper()) {
25250 nextSiblingDom = translatable.getWrapper().dom;
25251 }
25252 else {
25253 nextSiblingDom = nextSibling ? nextSibling.element.dom : null;
25254 }
25255 }
25256
25257 containerDom.insertBefore(itemDom, nextSiblingDom);
25258
25259 return this;
25260 },
25261
25262 insertBodyItem: function(item) {
25263 var container = this.container.setUseBodyElement(true),
25264 bodyDom = container.bodyElement.dom;
25265
25266 if (item.getZIndex() === null) {
25267 item.setZIndex((container.indexOf(item) + 1) * 2);
25268 }
25269
25270 bodyDom.insertBefore(item.element.dom, bodyDom.firstChild);
25271
25272 return this;
25273 },
25274
25275 removeInnerItem: function(item) {
25276 item.element.detach();
25277 },
25278
25279 removeBodyItem: function(item) {
25280 item.setZIndex(null);
25281 item.element.detach();
25282 },
25283
25284 onItemRemove: function(item, index, destroying) {
25285 var docked = item.getDocked();
25286
25287 if (docked) {
25288 this.undockItem(item);
25289 }
25290 else if (item.isCentered()) {
25291 this.onItemCenteredChange(item, false);
25292 }
25293 else if (item.isFloating()) {
25294 this.onItemFloatingChange(item, false);
25295 }
25296 else {
25297 this.onItemInnerStateChange(item, false, destroying);
25298 }
25299 },
25300
25301 onItemMove: function(item, toIndex, fromIndex) {
25302 if (item.isCentered() || item.isFloating()) {
25303 item.setZIndex((toIndex + 1) * 2);
25304 }
25305 else if (item.isInnerItem()) {
25306 this.insertInnerItem(item, this.container.innerIndexOf(item));
25307 }
25308 else {
25309 this.undockItem(item);
25310 this.dockItem(item);
25311 }
25312 },
25313
25314 onItemCenteredChange: function(item, centered) {
25315 var wrapperName = '$centerWrapper';
25316
25317 if (centered) {
25318 this.insertBodyItem(item);
25319 item.link(wrapperName, new Ext.util.Wrapper({
25320 className: this.centerWrapperClass
25321 }, item.element));
25322 }
25323 else {
25324 item.unlink(wrapperName);
25325 this.removeBodyItem(item);
25326 }
25327 },
25328
25329 onItemFloatingChange: function(item, floating) {
25330 if (floating) {
25331 this.insertBodyItem(item);
25332 }
25333 else {
25334 this.removeBodyItem(item);
25335 }
25336 },
25337
25338 onBeforeItemDockedChange: function(item, docked, oldDocked) {
25339 if (oldDocked) {
25340 this.undockItem(item);
25341 }
25342 },
25343
25344 onAfterItemDockedChange: function(item, docked, oldDocked) {
25345 if (docked) {
25346 this.dockItem(item);
25347 }
25348 },
25349
25350 onContainerSizeStateChange: function() {
25351 var dockWrapper = this.getDockWrapper();
25352
25353 if (dockWrapper) {
25354 dockWrapper.setSizeState(this.container.getSizeState());
25355 }
25356 },
25357
25358 onContainerSizeFlagsChange: function() {
25359 var items = this.dockedItems,
25360 i, ln, item;
25361
25362 for (i = 0, ln = items.length; i < ln; i++) {
25363 item = items[i];
25364 this.refreshDockedItemLayoutSizeFlags(item);
25365 }
25366 },
25367
25368 refreshDockedItemLayoutSizeFlags: function(item) {
25369 var container = this.container,
25370 dockedDirection = this.positionDirectionMap[item.getDocked()],
25371 binaryMask = (dockedDirection === 'horizontal') ? container.LAYOUT_HEIGHT : container.LAYOUT_WIDTH,
25372 flags = (container.getSizeFlags() & binaryMask);
25373
25374 item.setLayoutSizeFlags(flags);
25375 },
25376
25377 dockItem: function(item) {
25378 var DockClass = Ext.layout.wrapper.BoxDock,
25379 dockedItems = this.dockedItems,
25380 ln = dockedItems.length,
25381 container = this.container,
25382 itemIndex = container.indexOf(item),
25383 positionDirectionMap = this.positionDirectionMap,
25384 direction = positionDirectionMap[item.getDocked()],
25385 dockInnerWrapper = this.dockInnerWrapper,
25386 referenceDirection, i, dockedItem, index, previousItem, slice,
25387 referenceItem, referenceDocked, referenceWrapper, newWrapper, nestedWrapper, oldInnerWrapper;
25388
25389 this.monitorSizeStateChange();
25390 this.monitorSizeFlagsChange();
25391
25392 if (!dockInnerWrapper) {
25393 dockInnerWrapper = this.link('dockInnerWrapper', new Ext.layout.wrapper.Inner({
25394 container: this.container
25395 }));
25396 }
25397
25398 if (ln === 0) {
25399 dockedItems.push(item);
25400
25401 newWrapper = new DockClass({
25402 container: this.container,
25403 direction: direction
25404 });
25405
25406 newWrapper.addItem(item);
25407 newWrapper.getElement().replace(dockInnerWrapper.getElement());
25408 newWrapper.setInnerWrapper(dockInnerWrapper);
25409 container.onInitialized('onContainerSizeStateChange', this);
25410 }
25411 else {
25412 for (i = 0; i < ln; i++) {
25413 dockedItem = dockedItems[i];
25414 index = container.indexOf(dockedItem);
25415
25416 if (index > itemIndex) {
25417 referenceItem = previousItem || dockedItems[0];
25418 dockedItems.splice(i, 0, item);
25419 break;
25420 }
25421
25422 previousItem = dockedItem;
25423 }
25424
25425 if (!referenceItem) {
25426 referenceItem = dockedItems[ln - 1];
25427 dockedItems.push(item);
25428 }
25429
25430 referenceDocked = referenceItem.getDocked();
25431 referenceWrapper = referenceItem.$dockWrapper;
25432 referenceDirection = positionDirectionMap[referenceDocked];
25433
25434 if (direction === referenceDirection) {
25435 referenceWrapper.addItem(item);
25436 }
25437 else {
25438 slice = referenceWrapper.getItemsSlice(itemIndex);
25439
25440 newWrapper = new DockClass({
25441 container: this.container,
25442 direction: direction
25443 });
25444
25445 if (slice.length > 0) {
25446 if (slice.length === referenceWrapper.itemsCount) {
25447 nestedWrapper = referenceWrapper;
25448 newWrapper.setSizeState(nestedWrapper.getSizeState());
25449 newWrapper.getElement().replace(nestedWrapper.getElement());
25450 }
25451 else {
25452 nestedWrapper = new DockClass({
25453 container: this.container,
25454 direction: referenceDirection
25455 });
25456 nestedWrapper.setInnerWrapper(referenceWrapper.getInnerWrapper());
25457 nestedWrapper.addItems(slice);
25458 referenceWrapper.setInnerWrapper(newWrapper);
25459 }
25460
25461 newWrapper.setInnerWrapper(nestedWrapper);
25462 }
25463 else {
25464 oldInnerWrapper = referenceWrapper.getInnerWrapper();
25465 referenceWrapper.setInnerWrapper(null);
25466 newWrapper.setInnerWrapper(oldInnerWrapper);
25467 referenceWrapper.setInnerWrapper(newWrapper);
25468 }
25469
25470 newWrapper.addItem(item);
25471 }
25472 }
25473
25474 container.onInitialized('refreshDockedItemLayoutSizeFlags', this, [item]);
25475 },
25476
25477 getDockWrapper: function() {
25478 var dockedItems = this.dockedItems;
25479
25480 if (dockedItems.length > 0) {
25481 return dockedItems[0].$dockWrapper;
25482 }
25483
25484 return null;
25485 },
25486
25487 undockItem: function(item) {
25488 var dockedItems = this.dockedItems;
25489
25490 if (item.$dockWrapper) {
25491 item.$dockWrapper.removeItem(item);
25492 }
25493
25494 Ext.Array.remove(dockedItems, item);
25495
25496 item.setLayoutSizeFlags(0);
25497 },
25498
25499 destroy: function() {
25500 this.dockedItems.length = 0;
25501
25502 delete this.dockedItems;
25503
25504 this.callSuper();
25505 }
25506 });
25507
25508 /**
25509 * Box is a superclass for the two box layouts:
25510 *
25511 * * {@link Ext.layout.HBox hbox}
25512 * * {@link Ext.layout.VBox vbox}
25513 *
25514 * Box itself is never used directly, but its subclasses provide flexible arrangement of child components
25515 * inside a {@link Ext.Container Container}.
25516 *
25517 * ## Horizontal Box
25518 *
25519 * HBox allows you to easily lay out child components horizontally. It can size items based on a fixed width or a
25520 * fraction of the total width available, enabling you to achieve flexible layouts that expand or contract to fill the
25521 * space available.
25522 *
25523 * {@img ../guides/layouts/hbox.jpg}
25524 *
25525 * See the {@link Ext.layout.HBox HBox layout docs} for more information on using hboxes.
25526 *
25527 * ## Vertical Box
25528 *
25529 * VBox allows you to easily lay out child components vertically. It can size items based on a fixed height or a
25530 * fraction of the total height available, enabling you to achieve flexible layouts that expand or contract to fill the
25531 * space available.
25532 *
25533 * {@img ../guides/layouts/vbox.jpg}
25534 *
25535 * See the {@link Ext.layout.VBox VBox layout docs} for more information on using vboxes.
25536 *
25537 * For a more detailed overview of Sencha Touch 2 layouts, check out the
25538 * [Layout Guide](../../../core_concepts/layouts.html).
25539 */
25540 Ext.define('Ext.layout.Box', {
25541 extend: Ext.layout.Default ,
25542
25543 config: {
25544 orient: 'horizontal',
25545
25546 /**
25547 * @cfg {String} align
25548 * Controls how the child items of the container are aligned. Acceptable configuration values for this property are:
25549 *
25550 * - ** start ** : child items are packed together at left side of container
25551 * - ** center ** : child items are packed together at mid-width of container
25552 * - ** end ** : child items are packed together at right side of container
25553 * - **stretch** : child items are stretched vertically to fill the height of the container
25554 *
25555 * Please see the 'Pack and Align' section of the [Events Guide](../../../core_concepts/events.html) for a detailed example and
25556 * explanation.
25557 * @accessor
25558 */
25559 align: 'start',
25560
25561 /**
25562 * @cfg {String} pack
25563 * Controls how the child items of the container are packed together. Acceptable configuration values
25564 * for this property are:
25565 *
25566 * - ** start ** : child items are packed together at left side of container
25567 * - ** center ** : child items are packed together at mid-width of container
25568 * - ** end ** : child items are packed together at right side of container
25569 * - ** justify ** : child items are packed evenly across the container. Uses the 'justify-content: space-between' css property
25570 *
25571 * Please see the 'Pack and Align' section of the [Events Guide](../../../core_concepts/events.html) for a detailed example and
25572 * explanation.
25573 * @accessor
25574 */
25575 pack: 'start'
25576 },
25577
25578 alias: 'layout.tablebox',
25579
25580 layoutBaseClass: 'x-layout-tablebox',
25581
25582 itemClass: 'x-layout-tablebox-item',
25583
25584 setContainer: function(container) {
25585 this.callSuper(arguments);
25586
25587 container.innerElement.addCls(this.layoutBaseClass);
25588
25589 container.on('flexchange', 'onItemFlexChange', this, {
25590 delegate: '> component'
25591 });
25592 },
25593
25594 onItemInnerStateChange: function(item, isInner) {
25595 this.callSuper(arguments);
25596
25597 item.toggleCls(this.itemClass, isInner);
25598 },
25599
25600 onItemFlexChange: function() {
25601
25602 }
25603 });
25604
25605 /**
25606 * AbstractBox is a superclass for the two box layouts:
25607 *
25608 * * {@link Ext.layout.HBox hbox}
25609 * * {@link Ext.layout.VBox vbox}
25610 *
25611 * FlexBox itself is never used directly, but its subclasses provide flexible arrangement of child components
25612 * inside a {@link Ext.Container Container}.
25613 *
25614 * ## Horizontal Box
25615 *
25616 * HBox allows you to easily lay out child components horizontally. It can size items based on a fixed width or a
25617 * fraction of the total width available, enabling you to achieve flexible layouts that expand or contract to fill the
25618 * space available.
25619 *
25620 * {@img ../guides/layouts/hbox.jpg}
25621 *
25622 * See the {@link Ext.layout.HBox HBox layout docs} for more information on using hboxes.
25623 *
25624 * ## Vertical Box
25625 *
25626 * VBox allows you to easily lay out child components vertically. It can size items based on a fixed height or a
25627 * fraction of the total height available, enabling you to achieve flexible layouts that expand or contract to fill the
25628 * space available.
25629 *
25630 * {@img ../guides/layouts/vbox.jpg}
25631 *
25632 * See the {@link Ext.layout.VBox VBox layout docs} for more information on using vboxes.
25633 *
25634 * For a more detailed overview of Sencha Touch 2 layouts, check out the
25635 * [Layout Guide](../../../core_concepts/layouts.html).
25636 */
25637 Ext.define('Ext.layout.FlexBox', {
25638 extend: Ext.layout.Box ,
25639
25640 alias: 'layout.box',
25641
25642 config: {
25643 align: 'stretch'
25644 },
25645
25646 layoutBaseClass: 'x-layout-box',
25647
25648 itemClass: 'x-layout-box-item',
25649
25650 setContainer: function(container) {
25651 this.callSuper(arguments);
25652
25653 this.monitorSizeFlagsChange();
25654 },
25655
25656 applyOrient: function(orient) {
25657 //<debug error>
25658 if (orient !== 'horizontal' && orient !== 'vertical') {
25659 Ext.Logger.error("Invalid box orient of: '" + orient + "', must be either 'horizontal' or 'vertical'");
25660 }
25661 //</debug>
25662
25663 return orient;
25664 },
25665
25666 updateOrient: function(orient, oldOrient) {
25667 var container = this.container,
25668 delegation = {
25669 delegate: '> component'
25670 };
25671
25672 if (orient === 'horizontal') {
25673 this.sizePropertyName = 'width';
25674 }
25675 else {
25676 this.sizePropertyName = 'height';
25677 }
25678
25679 container.innerElement.swapCls('x-' + orient, 'x-' + oldOrient);
25680
25681 if (oldOrient) {
25682 container.un(oldOrient === 'horizontal' ? 'widthchange' : 'heightchange', 'onItemSizeChange', this, delegation);
25683 this.redrawContainer();
25684 }
25685
25686 container.on(orient === 'horizontal' ? 'widthchange' : 'heightchange', 'onItemSizeChange', this, delegation);
25687 },
25688
25689 onItemInnerStateChange: function(item, isInner) {
25690 this.callSuper(arguments);
25691
25692 var flex, size;
25693
25694 item.toggleCls(this.itemClass, isInner);
25695
25696 if (isInner) {
25697 flex = item.getFlex();
25698 size = item.get(this.sizePropertyName);
25699
25700 if (flex) {
25701 this.doItemFlexChange(item, flex);
25702 }
25703 else if (size) {
25704 this.doItemSizeChange(item, size);
25705 }
25706 }
25707
25708 this.refreshItemSizeState(item);
25709 },
25710
25711 refreshItemSizeState: function(item) {
25712 var isInner = item.isInnerItem(),
25713 container = this.container,
25714 LAYOUT_HEIGHT = container.LAYOUT_HEIGHT,
25715 LAYOUT_WIDTH = container.LAYOUT_WIDTH,
25716 dimension = this.sizePropertyName,
25717 layoutSizeFlags = 0,
25718 containerSizeFlags = container.getSizeFlags();
25719
25720 if (isInner) {
25721 layoutSizeFlags |= container.LAYOUT_STRETCHED;
25722
25723 if (this.getAlign() === 'stretch') {
25724 layoutSizeFlags |= containerSizeFlags & (dimension === 'width' ? LAYOUT_HEIGHT : LAYOUT_WIDTH);
25725 }
25726
25727 if (item.getFlex()) {
25728 layoutSizeFlags |= containerSizeFlags & (dimension === 'width' ? LAYOUT_WIDTH : LAYOUT_HEIGHT);
25729 }
25730 }
25731
25732 item.setLayoutSizeFlags(layoutSizeFlags);
25733 },
25734
25735 refreshAllItemSizedStates: function() {
25736 var innerItems = this.container.innerItems,
25737 i, ln, item;
25738
25739 for (i = 0,ln = innerItems.length; i < ln; i++) {
25740 item = innerItems[i];
25741 this.refreshItemSizeState(item);
25742 }
25743 },
25744
25745 onContainerSizeFlagsChange: function() {
25746 this.refreshAllItemSizedStates();
25747
25748 this.callSuper(arguments);
25749 },
25750
25751 onItemSizeChange: function(item, size) {
25752 if (item.isInnerItem()) {
25753 this.doItemSizeChange(item, size);
25754 }
25755 },
25756
25757 doItemSizeChange: function(item, size) {
25758 if (size) {
25759 item.setFlex(null);
25760 this.redrawContainer();
25761 }
25762 },
25763
25764 onItemFlexChange: function(item, flex) {
25765 if (item.isInnerItem()) {
25766 this.doItemFlexChange(item, flex);
25767 this.refreshItemSizeState(item);
25768 }
25769 },
25770
25771 doItemFlexChange: function(item, flex) {
25772 this.setItemFlex(item, flex);
25773
25774 if (flex) {
25775 item.set(this.sizePropertyName, null);
25776 }
25777 else {
25778 this.redrawContainer();
25779 }
25780 },
25781
25782 redrawContainer: function() {
25783 var container = this.container,
25784 renderedTo = container.element.dom.parentNode;
25785
25786 if (renderedTo && renderedTo.nodeType !== 11) {
25787 container.innerElement.redraw();
25788 }
25789 },
25790
25791 /**
25792 * Sets the flex of an item in this box layout.
25793 * @param {Ext.Component} item The item of this layout which you want to update the flex of.
25794 * @param {Number} flex The flex to set on this method
25795 */
25796 setItemFlex: function(item, flex) {
25797 var element = item.element;
25798
25799 element.toggleCls('x-flexed', !!flex);
25800
25801 if (!flex) {
25802 flex = '';
25803 }
25804 else {
25805 flex = String(flex);
25806 }
25807
25808 if (Ext.browser.is.WebKit) {
25809 element.dom.style.setProperty('-webkit-box-flex', flex, null);
25810 }
25811 else if (Ext.browser.is.IE) {
25812 element.dom.style.setProperty('-ms-flex', flex + ' 0 0px', null);
25813 }
25814 else {
25815 element.dom.style.setProperty('flex', flex + ' 0 0px', null);
25816 }
25817 },
25818
25819 convertPosition: function(position) {
25820 var positionMap = this.positionMap;
25821
25822 if (positionMap.hasOwnProperty(position)) {
25823 return positionMap[position];
25824 }
25825
25826 return position;
25827 },
25828
25829 applyAlign: function(align) {
25830 return this.convertPosition(align);
25831 },
25832
25833 updateAlign: function(align, oldAlign) {
25834 var container = this.container;
25835
25836 container.innerElement.swapCls(align, oldAlign, true, 'x-align');
25837
25838 if (oldAlign !== undefined) {
25839 this.refreshAllItemSizedStates();
25840 }
25841 },
25842
25843 applyPack: function(pack) {
25844 return this.convertPosition(pack);
25845 },
25846
25847 updatePack: function(pack, oldPack) {
25848 this.container.innerElement.swapCls(pack, oldPack, true, 'x-pack');
25849 }
25850 });
25851
25852 /**
25853 * The HBox (short for horizontal box) layout makes it easy to position items horizontally in a
25854 * {@link Ext.Container Container}. It can size items based on a fixed width or a fraction of the total width
25855 * available.
25856 *
25857 * For example, an email client might have a list of messages pinned to the left, taking say one third of the available
25858 * width, and a message viewing panel in the rest of the screen. We can achieve this with hbox layout's *flex* config:
25859 *
25860 * @example
25861 * Ext.create('Ext.Container', {
25862 * fullscreen: true,
25863 * layout: 'hbox',
25864 * items: [
25865 * {
25866 * html: 'message list',
25867 * style: 'background-color: #5E99CC;',
25868 * flex: 1
25869 * },
25870 * {
25871 * html: 'message preview',
25872 * style: 'background-color: #759E60;',
25873 * flex: 2
25874 * }
25875 * ]
25876 * });
25877 *
25878 * This will give us two boxes - one that's one third of the available width, the other being two thirds of the
25879 * available width:
25880 *
25881 * {@img ../guides/layouts/hbox.jpg}
25882 *
25883 * We can also specify fixed widths for child items, or mix fixed widths and flexes. For example, here we have 3 items
25884 * - one on each side with flex: 1, and one in the center with a fixed width of 100px:
25885 *
25886 * @example
25887 * Ext.create('Ext.Container', {
25888 * fullscreen: true,
25889 * layout: 'hbox',
25890 * items: [
25891 * {
25892 * html: 'Left item',
25893 * style: 'background-color: #759E60;',
25894 * flex: 1
25895 * },
25896 * {
25897 * html: 'Center item',
25898 * width: 100
25899 * },
25900 * {
25901 * html: 'Right item',
25902 * style: 'background-color: #5E99CC;',
25903 * flex: 1
25904 * }
25905 * ]
25906 * });
25907 *
25908 * Which gives us an effect like this:
25909 *
25910 * {@img ../guides/layouts/hboxfixed.jpg}
25911 *
25912 * For a more detailed overview of Sencha Touch 2 layouts, check out the
25913 * [Layout Guide](../../../core_concepts/layouts.html).
25914 */
25915 Ext.define('Ext.layout.HBox', {
25916 extend: Ext.layout.FlexBox ,
25917
25918 alias: 'layout.hbox'
25919 });
25920
25921 /**
25922 *
25923 */
25924 Ext.define('Ext.layout.Fit', {
25925 extend: Ext.layout.Default ,
25926
25927 isFit: true,
25928
25929 alias: 'layout.fit',
25930
25931 layoutClass: 'x-layout-fit',
25932
25933 itemClass: 'x-layout-fit-item',
25934
25935 setContainer: function(container) {
25936 this.callSuper(arguments);
25937
25938 container.innerElement.addCls(this.layoutClass);
25939 this.onContainerSizeFlagsChange();
25940 this.monitorSizeFlagsChange();
25941 },
25942
25943 onContainerSizeFlagsChange: function() {
25944 var container = this.container,
25945 sizeFlags = container.getSizeFlags(),
25946 stretched = Boolean(sizeFlags & container.LAYOUT_STRETCHED),
25947 innerItems = container.innerItems,
25948 i, ln, item;
25949
25950 this.callSuper();
25951
25952 for (i = 0,ln = innerItems.length; i < ln; i++) {
25953 item = innerItems[i];
25954 item.setLayoutSizeFlags(sizeFlags);
25955 }
25956
25957 container.innerElement.toggleCls('x-stretched', stretched);
25958 },
25959
25960 onItemInnerStateChange: function(item, isInner) {
25961 this.callSuper(arguments);
25962 item.toggleCls(this.itemClass, isInner);
25963 item.setLayoutSizeFlags(isInner ? this.container.getSizeFlags() : 0);
25964 }
25965 });
25966
25967 /**
25968 *
25969 */
25970 Ext.define('Ext.layout.Float', {
25971 extend: Ext.layout.Default ,
25972
25973 alias: 'layout.float',
25974
25975 config: {
25976 direction: 'left'
25977 },
25978
25979 layoutClass: 'layout-float',
25980
25981 itemClass: 'layout-float-item',
25982
25983 setContainer: function(container) {
25984 this.callSuper(arguments);
25985
25986 container.innerElement.addCls(this.layoutClass);
25987 },
25988
25989 onItemInnerStateChange: function(item, isInner) {
25990 this.callSuper(arguments);
25991 item.toggleCls(this.itemClass, isInner);
25992 },
25993
25994 updateDirection: function(direction, oldDirection) {
25995 var prefix = 'direction-';
25996
25997 this.container.innerElement.swapCls(prefix + direction, prefix + oldDirection);
25998 }
25999 });
26000
26001 /**
26002 *
26003 */
26004 Ext.define('Ext.layout.wrapper.Dock', {
26005
26006
26007
26008
26009 config: {
26010 direction: 'horizontal',
26011 element: {
26012 className: 'x-dock'
26013 },
26014 bodyElement: {
26015 className: 'x-dock-body'
26016 },
26017 innerWrapper: null,
26018 sizeState: false,
26019 container: null
26020 },
26021
26022 positionMap: {
26023 top: 'start',
26024 left: 'start',
26025 bottom: 'end',
26026 right: 'end'
26027 },
26028
26029 constructor: function(config) {
26030 this.items = {
26031 start: [],
26032 end: []
26033 };
26034
26035 this.itemsCount = 0;
26036
26037 this.initConfig(config);
26038 },
26039
26040 addItems: function(items) {
26041 var i, ln, item;
26042
26043 for (i = 0, ln = items.length; i < ln; i++) {
26044 item = items[i];
26045 this.addItem(item);
26046 }
26047 },
26048
26049 addItem: function(item) {
26050 var docked = item.getDocked(),
26051 position = this.positionMap[docked],
26052 wrapper = item.$dockWrapper,
26053 container = this.getContainer(),
26054 index = container.indexOf(item),
26055 items = this.items,
26056 sideItems = items[position],
26057 itemWrapper, element, i, ln, sibling, referenceElement, siblingIndex;
26058
26059 if (wrapper) {
26060 wrapper.removeItem(item);
26061 }
26062
26063 item.$dockWrapper = this;
26064 itemWrapper = item.link('$dockItemWrapper', new Ext.util.Wrapper({
26065 className: 'x-dock-item'
26066 }));
26067 item.addCls('x-docked-' + docked);
26068 element = itemWrapper.element;
26069
26070 for (i = 0, ln = sideItems.length; i < ln; i++) {
26071 sibling = sideItems[i];
26072 siblingIndex = container.indexOf(sibling);
26073
26074 if (siblingIndex > index) {
26075 referenceElement = sibling.element;
26076 sideItems.splice(i, 0, item);
26077 break;
26078 }
26079 }
26080
26081 if (!referenceElement) {
26082 sideItems.push(item);
26083 referenceElement = this.getBodyElement();
26084 }
26085
26086 this.itemsCount++;
26087
26088 if (position === 'start') {
26089 element.insertBefore(referenceElement);
26090 }
26091 else {
26092 element.insertAfter(referenceElement);
26093 }
26094
26095 itemWrapper.wrap(item.element);
26096 itemWrapper.bindSize(this.getDirection() === 'horizontal' ? 'width' : 'height');
26097 },
26098
26099 removeItem: function(item) {
26100 var position = item.getDocked(),
26101 items = this.items[this.positionMap[position]];
26102
26103 item.removeCls('x-docked-' + position);
26104 Ext.Array.remove(items, item);
26105 item.unlink('$dockItemWrapper');
26106 item.element.detach();
26107 delete item.$dockWrapper;
26108
26109 if (--this.itemsCount === 0) {
26110 this.destroy();
26111 }
26112 },
26113
26114 getItemsSlice: function(index) {
26115 var container = this.getContainer(),
26116 items = this.items,
26117 slice = [],
26118 sideItems, i, ln, item;
26119
26120 for (sideItems = items.start, i = 0, ln = sideItems.length; i < ln; i++) {
26121 item = sideItems[i];
26122 if (container.indexOf(item) > index) {
26123 slice.push(item);
26124 }
26125 }
26126
26127 for (sideItems = items.end, i = 0, ln = sideItems.length; i < ln; i++) {
26128 item = sideItems[i];
26129 if (container.indexOf(item) > index) {
26130 slice.push(item);
26131 }
26132 }
26133
26134 return slice;
26135 },
26136
26137 applyElement: function(element) {
26138 return Ext.Element.create(element);
26139 },
26140
26141 updateElement: function(element) {
26142 element.addCls('x-dock-' + this.getDirection());
26143 },
26144
26145 applyBodyElement: function(bodyElement) {
26146 return Ext.Element.create(bodyElement);
26147 },
26148
26149 updateBodyElement: function(bodyElement) {
26150 this.getElement().append(bodyElement);
26151 },
26152
26153 updateInnerWrapper: function(innerWrapper, oldInnerWrapper) {
26154 var innerElement = this.getBodyElement();
26155
26156 if (oldInnerWrapper && oldInnerWrapper.$outerWrapper === this) {
26157 innerElement.remove(oldInnerWrapper.getElement());
26158 delete oldInnerWrapper.$outerWrapper;
26159 }
26160
26161 if (innerWrapper) {
26162 innerWrapper.setSizeState(this.getSizeState());
26163 innerWrapper.$outerWrapper = this;
26164 innerElement.append(innerWrapper.getElement());
26165 }
26166 },
26167
26168 updateSizeState: function(state) {
26169 var innerWrapper = this.getInnerWrapper();
26170
26171 this.getElement().setSizeState(state);
26172
26173 if (innerWrapper) {
26174 innerWrapper.setSizeState(state);
26175 }
26176 },
26177
26178 destroy: function() {
26179 var innerWrapper = this.getInnerWrapper(),
26180 outerWrapper = this.$outerWrapper;
26181
26182 if (innerWrapper) {
26183 if (outerWrapper) {
26184 outerWrapper.setInnerWrapper(innerWrapper);
26185 }
26186 else {
26187 innerWrapper.getElement().replace(this.getElement());
26188 delete innerWrapper.$outerWrapper;
26189 }
26190 }
26191
26192 delete this.$outerWrapper;
26193
26194 this.setInnerWrapper(null);
26195
26196 this.unlink('_bodyElement', '_element');
26197
26198 this.callSuper();
26199 }
26200 });
26201
26202 /**
26203 * The VBox (short for vertical box) layout makes it easy to position items horizontally in a
26204 * {@link Ext.Container Container}. It can size items based on a fixed height or a fraction of the total height
26205 * available.
26206 *
26207 * For example, let's say we want a banner to take one third of the available height, and an information panel in the
26208 * rest of the screen. We can achieve this with vbox layout's *flex* config:
26209 *
26210 * @example
26211 * Ext.create('Ext.Container', {
26212 * fullscreen: true,
26213 * layout: 'vbox',
26214 * items: [
26215 * {
26216 * html: 'Awesome banner',
26217 * style: 'background-color: #759E60;',
26218 * flex: 1
26219 * },
26220 * {
26221 * html: 'Some wonderful information',
26222 * style: 'background-color: #5E99CC;',
26223 * flex: 2
26224 * }
26225 * ]
26226 * });
26227 *
26228 * This will give us two boxes - one that's one third of the available height, the other being two thirds of the
26229 * available height:
26230 *
26231 * {@img ../guides/layouts/vbox.jpg}
26232 *
26233 * We can also specify fixed heights for child items, or mix fixed heights and flexes. For example, here we have 3
26234 * items - one at the top and bottom with flex: 1, and one in the center with a fixed width of 100px:
26235 *
26236 * @example preview portrait
26237 * Ext.create('Ext.Container', {
26238 * fullscreen: true,
26239 * layout: 'vbox',
26240 * items: [
26241 * {
26242 * html: 'Top item',
26243 * style: 'background-color: #5E99CC;',
26244 * flex: 1
26245 * },
26246 * {
26247 * html: 'Center item',
26248 * height: 100
26249 * },
26250 * {
26251 * html: 'Bottom item',
26252 * style: 'background-color: #759E60;',
26253 * flex: 1
26254 * }
26255 * ]
26256 * });
26257 *
26258 * Which gives us an effect like this:
26259 *
26260 * {@img ../guides/layouts/vboxfixed.jpg}
26261 *
26262 * For a more detailed overview of Sencha Touch 2 layouts, check out the
26263 * [Layout Guide](../../../core_concepts/layouts.html).
26264 *
26265 */
26266 Ext.define('Ext.layout.VBox', {
26267 extend: Ext.layout.FlexBox ,
26268
26269 alias: 'layout.vbox',
26270
26271 config: {
26272 orient: 'vertical'
26273 }
26274 });
26275
26276 /**
26277 * @private
26278 */
26279 Ext.define('Ext.fx.layout.card.Abstract', {
26280 extend: Ext.Evented ,
26281 isAnimation: true,
26282
26283 config: {
26284 direction: 'left',
26285
26286 duration: null,
26287
26288 reverse: null,
26289
26290 layout: null
26291 },
26292
26293 updateLayout: function() {
26294 this.enable();
26295 },
26296
26297 enable: function() {
26298 var layout = this.getLayout();
26299
26300 if (layout) {
26301 layout.onBefore('activeitemchange', 'onActiveItemChange', this);
26302 }
26303 },
26304
26305 disable: function() {
26306 var layout = this.getLayout();
26307
26308 if (this.isAnimating) {
26309 this.stopAnimation();
26310 }
26311
26312 if (layout) {
26313 layout.unBefore('activeitemchange', 'onActiveItemChange', this);
26314 }
26315 },
26316
26317 onActiveItemChange: Ext.emptyFn,
26318
26319 destroy: function() {
26320 var layout = this.getLayout();
26321
26322 if (this.isAnimating) {
26323 this.stopAnimation();
26324 }
26325
26326 if (layout) {
26327 layout.unBefore('activeitemchange', 'onActiveItemChange', this);
26328 }
26329 this.setLayout(null);
26330
26331 if (this.observableId) {
26332 this.fireEvent('destroy', this);
26333 this.clearListeners();
26334 this.clearManagedListeners();
26335 }
26336
26337 // this.callSuper(arguments);
26338 }
26339 });
26340
26341 /**
26342 * @private
26343 */
26344 Ext.define('Ext.fx.State', {
26345
26346 isAnimatable: {
26347 'background-color' : true,
26348 'background-image' : true,
26349 'background-position': true,
26350 'border-bottom-color': true,
26351 'border-bottom-width': true,
26352 'border-color' : true,
26353 'border-left-color' : true,
26354 'border-left-width' : true,
26355 'border-right-color' : true,
26356 'border-right-width' : true,
26357 'border-spacing' : true,
26358 'border-top-color' : true,
26359 'border-top-width' : true,
26360 'border-width' : true,
26361 'bottom' : true,
26362 'color' : true,
26363 'crop' : true,
26364 'font-size' : true,
26365 'font-weight' : true,
26366 'height' : true,
26367 'left' : true,
26368 'letter-spacing' : true,
26369 'line-height' : true,
26370 'margin-bottom' : true,
26371 'margin-left' : true,
26372 'margin-right' : true,
26373 'margin-top' : true,
26374 'max-height' : true,
26375 'max-width' : true,
26376 'min-height' : true,
26377 'min-width' : true,
26378 'opacity' : true,
26379 'outline-color' : true,
26380 'outline-offset' : true,
26381 'outline-width' : true,
26382 'padding-bottom' : true,
26383 'padding-left' : true,
26384 'padding-right' : true,
26385 'padding-top' : true,
26386 'right' : true,
26387 'text-indent' : true,
26388 'text-shadow' : true,
26389 'top' : true,
26390 'vertical-align' : true,
26391 'visibility' : true,
26392 'width' : true,
26393 'word-spacing' : true,
26394 'z-index' : true,
26395 'zoom' : true,
26396 'transform' : true
26397 },
26398
26399 constructor: function(data) {
26400 this.data = {};
26401
26402 this.set(data);
26403 },
26404
26405 setConfig: function(data) {
26406 this.set(data);
26407
26408 return this;
26409 },
26410
26411 setRaw: function(data) {
26412 this.data = data;
26413
26414 return this;
26415 },
26416
26417 clear: function() {
26418 return this.setRaw({});
26419 },
26420
26421 setTransform: function(name, value) {
26422 var data = this.data,
26423 isArray = Ext.isArray(value),
26424 transform = data.transform,
26425 ln, key;
26426
26427 if (!transform) {
26428 transform = data.transform = {
26429 translateX: 0,
26430 translateY: 0,
26431 translateZ: 0,
26432 scaleX: 1,
26433 scaleY: 1,
26434 scaleZ: 1,
26435 rotate: 0,
26436 rotateX: 0,
26437 rotateY: 0,
26438 rotateZ: 0,
26439 skewX: 0,
26440 skewY: 0
26441 };
26442 }
26443
26444 if (typeof name == 'string') {
26445 switch (name) {
26446 case 'translate':
26447 if (isArray) {
26448 ln = value.length;
26449
26450 if (ln == 0) { break; }
26451
26452 transform.translateX = value[0];
26453
26454 if (ln == 1) { break; }
26455
26456 transform.translateY = value[1];
26457
26458 if (ln == 2) { break; }
26459
26460 transform.translateZ = value[2];
26461 }
26462 else {
26463 transform.translateX = value;
26464 }
26465 break;
26466
26467 case 'rotate':
26468 if (isArray) {
26469 ln = value.length;
26470
26471 if (ln == 0) { break; }
26472
26473 transform.rotateX = value[0];
26474
26475 if (ln == 1) { break; }
26476
26477 transform.rotateY = value[1];
26478
26479 if (ln == 2) { break; }
26480
26481 transform.rotateZ = value[2];
26482 }
26483 else {
26484 transform.rotate = value;
26485 }
26486 break;
26487
26488
26489 case 'scale':
26490 if (isArray) {
26491 ln = value.length;
26492
26493 if (ln == 0) { break; }
26494
26495 transform.scaleX = value[0];
26496
26497 if (ln == 1) { break; }
26498
26499 transform.scaleY = value[1];
26500
26501 if (ln == 2) { break; }
26502
26503 transform.scaleZ = value[2];
26504 }
26505 else {
26506 transform.scaleX = value;
26507 transform.scaleY = value;
26508 }
26509 break;
26510
26511 case 'skew':
26512 if (isArray) {
26513 ln = value.length;
26514
26515 if (ln == 0) { break; }
26516
26517 transform.skewX = value[0];
26518
26519 if (ln == 1) { break; }
26520
26521 transform.skewY = value[1];
26522 }
26523 else {
26524 transform.skewX = value;
26525 }
26526 break;
26527
26528 default:
26529 transform[name] = value;
26530 }
26531 }
26532 else {
26533 for (key in name) {
26534 if (name.hasOwnProperty(key)) {
26535 value = name[key];
26536
26537 this.setTransform(key, value);
26538 }
26539 }
26540 }
26541 },
26542
26543 set: function(name, value) {
26544 var data = this.data,
26545 key;
26546
26547 if (typeof name != 'string') {
26548 for (key in name) {
26549 value = name[key];
26550
26551 if (key === 'transform') {
26552 this.setTransform(value);
26553 }
26554 else {
26555 data[key] = value;
26556 }
26557 }
26558 }
26559 else {
26560 if (name === 'transform') {
26561 this.setTransform(value);
26562 }
26563 else {
26564 data[name] = value;
26565 }
26566 }
26567
26568 return this;
26569 },
26570
26571 unset: function(name) {
26572 var data = this.data;
26573
26574 if (data.hasOwnProperty(name)) {
26575 delete data[name];
26576 }
26577
26578 return this;
26579 },
26580
26581 getData: function() {
26582 return this.data;
26583 }
26584 });
26585
26586
26587
26588 /**
26589 * @private
26590 */
26591 Ext.define('Ext.fx.animation.Abstract', {
26592
26593 extend: Ext.Evented ,
26594
26595 isAnimation: true,
26596
26597
26598
26599
26600
26601 config: {
26602 name: '',
26603
26604 element: null,
26605
26606 /**
26607 * @cfg
26608 * Before configuration.
26609 */
26610 before: null,
26611
26612 from: {},
26613
26614 to: {},
26615
26616 after: null,
26617
26618 states: {},
26619
26620 duration: 300,
26621
26622 /**
26623 * @cfg
26624 * Easing type.
26625 */
26626 easing: 'linear',
26627
26628 iteration: 1,
26629
26630 direction: 'normal',
26631
26632 delay: 0,
26633
26634 onBeforeStart: null,
26635
26636 onEnd: null,
26637
26638 onBeforeEnd: null,
26639
26640 scope: null,
26641
26642 reverse: null,
26643
26644 preserveEndState: false,
26645
26646 replacePrevious: true
26647 },
26648
26649 STATE_FROM: '0%',
26650
26651 STATE_TO: '100%',
26652
26653 DIRECTION_UP: 'up',
26654
26655 DIRECTION_DOWN: 'down',
26656
26657 DIRECTION_LEFT: 'left',
26658
26659 DIRECTION_RIGHT: 'right',
26660
26661 stateNameRegex: /^(?:[\d\.]+)%$/,
26662
26663 constructor: function() {
26664 this.states = {};
26665
26666 this.callParent(arguments);
26667
26668 return this;
26669 },
26670
26671 applyElement: function(element) {
26672 return Ext.get(element);
26673 },
26674
26675 applyBefore: function(before, current) {
26676 if (before) {
26677 return Ext.factory(before, Ext.fx.State, current);
26678 }
26679 },
26680
26681 applyAfter: function(after, current) {
26682 if (after) {
26683 return Ext.factory(after, Ext.fx.State, current);
26684 }
26685 },
26686
26687 setFrom: function(from) {
26688 return this.setState(this.STATE_FROM, from);
26689 },
26690
26691 setTo: function(to) {
26692 return this.setState(this.STATE_TO, to);
26693 },
26694
26695 getFrom: function() {
26696 return this.getState(this.STATE_FROM);
26697 },
26698
26699 getTo: function() {
26700 return this.getState(this.STATE_TO);
26701 },
26702
26703 setStates: function(states) {
26704 var validNameRegex = this.stateNameRegex,
26705 name;
26706
26707 for (name in states) {
26708 if (validNameRegex.test(name)) {
26709 this.setState(name, states[name]);
26710 }
26711 }
26712
26713 return this;
26714 },
26715
26716 getStates: function() {
26717 return this.states;
26718 },
26719
26720 stop: function() {
26721 this.fireEvent('stop', this);
26722 },
26723
26724 destroy: function() {
26725 this.stop();
26726 this.callParent();
26727 },
26728
26729 setState: function(name, state) {
26730 var states = this.getStates(),
26731 stateInstance;
26732
26733 stateInstance = Ext.factory(state, Ext.fx.State, states[name]);
26734
26735 if (stateInstance) {
26736 states[name] = stateInstance;
26737 }
26738 //<debug error>
26739 else if (name === this.STATE_TO) {
26740 Ext.Logger.error("Setting and invalid '100%' / 'to' state of: " + state);
26741 }
26742 //</debug>
26743
26744 return this;
26745 },
26746
26747 getState: function(name) {
26748 return this.getStates()[name];
26749 },
26750
26751 getData: function() {
26752 var states = this.getStates(),
26753 statesData = {},
26754 before = this.getBefore(),
26755 after = this.getAfter(),
26756 from = states[this.STATE_FROM],
26757 to = states[this.STATE_TO],
26758 fromData = from.getData(),
26759 toData = to.getData(),
26760 data, name, state;
26761
26762 for (name in states) {
26763 if (states.hasOwnProperty(name)) {
26764 state = states[name];
26765 data = state.getData();
26766 statesData[name] = data;
26767 }
26768 }
26769
26770 if (Ext.browser.is.AndroidStock2) {
26771 statesData['0.0001%'] = fromData;
26772 }
26773
26774 return {
26775 before: before ? before.getData() : {},
26776 after: after ? after.getData() : {},
26777 states: statesData,
26778 from: fromData,
26779 to: toData,
26780 duration: this.getDuration(),
26781 iteration: this.getIteration(),
26782 direction: this.getDirection(),
26783 easing: this.getEasing(),
26784 delay: this.getDelay(),
26785 onEnd: this.getOnEnd(),
26786 onBeforeEnd: this.getOnBeforeEnd(),
26787 onBeforeStart: this.getOnBeforeStart(),
26788 scope: this.getScope(),
26789 preserveEndState: this.getPreserveEndState(),
26790 replacePrevious: this.getReplacePrevious()
26791 };
26792 }
26793 });
26794
26795 /**
26796 * @private
26797 */
26798 Ext.define('Ext.fx.animation.Slide', {
26799
26800 extend: Ext.fx.animation.Abstract ,
26801
26802 alternateClassName: 'Ext.fx.animation.SlideIn',
26803
26804 alias: ['animation.slide', 'animation.slideIn'],
26805
26806 config: {
26807 /**
26808 * @cfg {String} direction The direction of which the slide animates
26809 * @accessor
26810 */
26811 direction: 'left',
26812
26813 /**
26814 * @cfg {Boolean} out True if you want to make this animation slide out, instead of slide in.
26815 * @accessor
26816 */
26817 out: false,
26818
26819 /**
26820 * @cfg {Number} offset The offset that the animation should go offscreen before entering (or when exiting)
26821 * @accessor
26822 */
26823 offset: 0,
26824
26825 /**
26826 * @cfg
26827 * @inheritdoc
26828 */
26829 easing: 'auto',
26830
26831 containerBox: 'auto',
26832
26833 elementBox: 'auto',
26834
26835 isElementBoxFit: true,
26836
26837 useCssTransform: true
26838 },
26839
26840 reverseDirectionMap: {
26841 up: 'down',
26842 down: 'up',
26843 left: 'right',
26844 right: 'left'
26845 },
26846
26847 applyEasing: function(easing) {
26848 if (easing === 'auto') {
26849 return 'ease-' + ((this.getOut()) ? 'in' : 'out');
26850 }
26851
26852 return easing;
26853 },
26854
26855 getContainerBox: function() {
26856 var box = this._containerBox;
26857
26858 if (box === 'auto') {
26859 box = this.getElement().getParent().getPageBox();
26860 }
26861
26862 return box;
26863 },
26864
26865 getElementBox: function() {
26866 var box = this._elementBox;
26867
26868 if (this.getIsElementBoxFit()) {
26869 return this.getContainerBox();
26870 }
26871
26872 if (box === 'auto') {
26873 box = this.getElement().getPageBox();
26874 }
26875
26876 return box;
26877 },
26878
26879 getData: function() {
26880 var elementBox = this.getElementBox(),
26881 containerBox = this.getContainerBox(),
26882 box = elementBox ? elementBox : containerBox,
26883 from = this.getFrom(),
26884 to = this.getTo(),
26885 out = this.getOut(),
26886 offset = this.getOffset(),
26887 direction = this.getDirection(),
26888 useCssTransform = this.getUseCssTransform(),
26889 reverse = this.getReverse(),
26890 translateX = 0,
26891 translateY = 0,
26892 fromX, fromY, toX, toY;
26893
26894 if (reverse) {
26895 direction = this.reverseDirectionMap[direction];
26896 }
26897
26898 switch (direction) {
26899 case this.DIRECTION_UP:
26900 if (out) {
26901 translateY = containerBox.top - box.top - box.height - offset;
26902 }
26903 else {
26904 translateY = containerBox.bottom - box.bottom + box.height + offset;
26905 }
26906
26907 break;
26908
26909 case this.DIRECTION_DOWN:
26910 if (out) {
26911 translateY = containerBox.bottom - box.bottom + box.height + offset;
26912 }
26913 else {
26914 translateY = containerBox.top - box.height - box.top - offset;
26915 }
26916
26917 break;
26918
26919 case this.DIRECTION_RIGHT:
26920 if (out) {
26921 translateX = containerBox.right - box.right + box.width + offset;
26922 }
26923 else {
26924 translateX = containerBox.left - box.left - box.width - offset;
26925 }
26926
26927 break;
26928
26929 case this.DIRECTION_LEFT:
26930 if (out) {
26931 translateX = containerBox.left - box.left - box.width - offset;
26932 }
26933 else {
26934 translateX = containerBox.right - box.right + box.width + offset;
26935 }
26936
26937 break;
26938 }
26939
26940 fromX = (out) ? 0 : translateX;
26941 fromY = (out) ? 0 : translateY;
26942
26943 if (useCssTransform) {
26944 from.setTransform({
26945 translateX: fromX,
26946 translateY: fromY
26947 });
26948 }
26949 else {
26950 from.set('left', fromX);
26951 from.set('top', fromY);
26952 }
26953
26954 toX = (out) ? translateX : 0;
26955 toY = (out) ? translateY : 0;
26956
26957 if (useCssTransform) {
26958 to.setTransform({
26959 translateX: toX,
26960 translateY: toY
26961 });
26962 }
26963 else {
26964 to.set('left', toX);
26965 to.set('top', toY);
26966 }
26967
26968 return this.callParent(arguments);
26969 }
26970 });
26971
26972 /**
26973 * @private
26974 */
26975 Ext.define('Ext.fx.animation.SlideOut', {
26976 extend: Ext.fx.animation.Slide ,
26977 alias: ['animation.slideOut'],
26978
26979 config: {
26980 // @hide
26981 out: true
26982 }
26983 });
26984
26985 /**
26986 * @private
26987 */
26988 Ext.define('Ext.fx.animation.Fade', {
26989 extend: Ext.fx.animation.Abstract ,
26990
26991 alternateClassName: 'Ext.fx.animation.FadeIn',
26992
26993 alias: ['animation.fade', 'animation.fadeIn'],
26994
26995 config: {
26996 /**
26997 * @cfg {Boolean} out True if you want to make this animation fade out, instead of fade in.
26998 * @accessor
26999 */
27000
27001 out: false,
27002
27003 before: {
27004 display: null,
27005 opacity: 0
27006 },
27007
27008 after: {
27009 opacity: null
27010 },
27011 reverse: null
27012 },
27013
27014 updateOut: function(newOut) {
27015 var to = this.getTo(),
27016 from = this.getFrom();
27017
27018 if (newOut) {
27019 from.set('opacity', 1);
27020 to.set('opacity', 0);
27021 } else {
27022 from.set('opacity', 0);
27023 to.set('opacity', 1);
27024 }
27025 }
27026 });
27027
27028 /**
27029 * @private
27030 */
27031 Ext.define('Ext.fx.animation.FadeOut', {
27032 extend: Ext.fx.animation.Fade ,
27033 alias: 'animation.fadeOut',
27034
27035 config: {
27036 // @hide
27037 out: true,
27038
27039 before: {}
27040 }
27041 });
27042
27043 /**
27044 * @private
27045 */
27046 Ext.define('Ext.fx.animation.Flip', {
27047 extend: Ext.fx.animation.Abstract ,
27048
27049 alias: 'animation.flip',
27050
27051 config: {
27052 easing: 'ease-in',
27053
27054 /**
27055 * @cfg {String} direction The direction of which the slide animates
27056 * @accessor
27057 */
27058 direction: 'right',
27059
27060 half: false,
27061
27062 out: null
27063 },
27064
27065 getData: function() {
27066 var from = this.getFrom(),
27067 to = this.getTo(),
27068 direction = this.getDirection(),
27069 out = this.getOut(),
27070 half = this.getHalf(),
27071 rotate = (half) ? 90 : 180,
27072 fromScale = 1,
27073 toScale = 1,
27074 fromRotateX = 0,
27075 fromRotateY = 0,
27076 toRotateX = 0,
27077 toRotateY = 0;
27078
27079
27080 if (out) {
27081 toScale = 0.8;
27082 }
27083 else {
27084 fromScale = 0.8;
27085 }
27086
27087 switch (direction) {
27088 case this.DIRECTION_UP:
27089 if (out) {
27090 toRotateX = rotate;
27091 }
27092 else {
27093 fromRotateX = -rotate;
27094 }
27095 break;
27096
27097 case this.DIRECTION_DOWN:
27098 if (out) {
27099 toRotateX = -rotate;
27100 }
27101 else {
27102 fromRotateX = rotate;
27103 }
27104 break;
27105
27106 case this.DIRECTION_RIGHT:
27107 if (out) {
27108 toRotateY = rotate;
27109 }
27110 else {
27111 fromRotateY = -rotate;
27112 }
27113 break;
27114
27115 case this.DIRECTION_LEFT:
27116 if (out) {
27117 toRotateY = -rotate;
27118 }
27119 else {
27120 fromRotateY = rotate;
27121 }
27122 break;
27123 }
27124
27125 from.setTransform({
27126 rotateX: fromRotateX,
27127 rotateY: fromRotateY,
27128 scale: fromScale
27129 });
27130
27131 to.setTransform({
27132 rotateX: toRotateX,
27133 rotateY: toRotateY,
27134 scale: toScale
27135 });
27136
27137 return this.callParent(arguments);
27138 }
27139 });
27140
27141 /**
27142 * @private
27143 */
27144 Ext.define('Ext.fx.animation.Pop', {
27145 extend: Ext.fx.animation.Abstract ,
27146
27147 alias: ['animation.pop', 'animation.popIn'],
27148
27149 alternateClassName: 'Ext.fx.animation.PopIn',
27150
27151 config: {
27152 /**
27153 * @cfg {Boolean} out True if you want to make this animation pop out, instead of pop in.
27154 * @accessor
27155 */
27156 out: false,
27157
27158 before: {
27159 display: null,
27160 opacity: 0
27161 },
27162 after: {
27163 opacity: null
27164 }
27165 },
27166
27167 getData: function() {
27168 var to = this.getTo(),
27169 from = this.getFrom(),
27170 out = this.getOut();
27171
27172 if (out) {
27173 from.set('opacity', 1);
27174 from.setTransform({
27175 scale: 1
27176 });
27177
27178 to.set('opacity', 0);
27179 to.setTransform({
27180 scale: 0
27181 });
27182 }
27183 else {
27184 from.set('opacity', 0);
27185 from.setTransform({
27186 scale: 0
27187 });
27188
27189 to.set('opacity', 1);
27190 to.setTransform({
27191 scale: 1
27192 });
27193 }
27194
27195 return this.callParent(arguments);
27196 }
27197 });
27198
27199 /**
27200 * @private
27201 */
27202 Ext.define('Ext.fx.animation.PopOut', {
27203 extend: Ext.fx.animation.Pop ,
27204
27205 alias: 'animation.popOut',
27206
27207 config: {
27208 // @hide
27209 out: true,
27210
27211 before: {}
27212 }
27213 });
27214
27215 /**
27216 * @private
27217 * @author Jacky Nguyen <jacky@sencha.com>
27218 *
27219 * This class is a factory class that will create and return an animation class based on the {@link #type} configuration.
27220 */
27221 Ext.define('Ext.fx.Animation', {
27222
27223
27224
27225
27226
27227
27228
27229
27230
27231
27232
27233 /**
27234 * @cfg {String} type The type of animation to use. The possible values are:
27235 *
27236 * - `fade` - {@link Ext.fx.animation.Fade}
27237 * - `fadeOut` - {@link Ext.fx.animation.FadeOut}
27238 * - `flip` - {@link Ext.fx.animation.Flip}
27239 * - `pop` - {@link Ext.fx.animation.Pop}
27240 * - `popOut` - {@link Ext.fx.animation.PopOut}
27241 * - `slide` - {@link Ext.fx.animation.Slide}
27242 * - `slideOut` - {@link Ext.fx.animation.SlideOut}
27243 */
27244
27245 constructor: function(config) {
27246 var defaultClass = Ext.fx.animation.Abstract,
27247 type;
27248
27249 if (typeof config == 'string') {
27250 type = config;
27251 config = {};
27252 }
27253 else if (config && config.type) {
27254 type = config.type;
27255 }
27256
27257 if (type) {
27258 if (Ext.browser.is.AndroidStock2) {
27259 if (type == 'pop') {
27260 type = 'fade';
27261 }
27262 if (type == 'popIn') {
27263 type = 'fadeIn';
27264 }
27265 if (type == 'popOut') {
27266 type = 'fadeOut';
27267 }
27268 }
27269 defaultClass = Ext.ClassManager.getByAlias('animation.' + type);
27270
27271 //<debug error>
27272 if (!defaultClass) {
27273 Ext.Logger.error("Invalid animation type of: '" + type + "'");
27274 }
27275 //</debug>
27276 }
27277
27278 return Ext.factory(config, defaultClass);
27279 }
27280 });
27281
27282 /**
27283 * @private
27284 */
27285 Ext.define('Ext.fx.layout.card.Style', {
27286
27287 extend: Ext.fx.layout.card.Abstract ,
27288
27289
27290
27291
27292
27293 config: {
27294 inAnimation: {
27295 before: {
27296 visibility: null
27297 },
27298 preserveEndState: false,
27299 replacePrevious: true
27300 },
27301
27302 outAnimation: {
27303 preserveEndState: false,
27304 replacePrevious: true
27305 }
27306 },
27307
27308 constructor: function(config) {
27309 var inAnimation, outAnimation;
27310
27311 this.initConfig(config);
27312
27313 this.endAnimationCounter = 0;
27314
27315 inAnimation = this.getInAnimation();
27316 outAnimation = this.getOutAnimation();
27317
27318 inAnimation.on('animationend', 'incrementEnd', this);
27319 outAnimation.on('animationend', 'incrementEnd', this);
27320 },
27321
27322 updateDirection: function(direction) {
27323 this.getInAnimation().setDirection(direction);
27324 this.getOutAnimation().setDirection(direction);
27325 },
27326
27327 updateDuration: function(duration) {
27328 this.getInAnimation().setDuration(duration);
27329 this.getOutAnimation().setDuration(duration);
27330 },
27331
27332 updateReverse: function(reverse) {
27333 this.getInAnimation().setReverse(reverse);
27334 this.getOutAnimation().setReverse(reverse);
27335 },
27336
27337 incrementEnd: function() {
27338 this.endAnimationCounter++;
27339
27340 if (this.endAnimationCounter > 1) {
27341 this.endAnimationCounter = 0;
27342 this.fireEvent('animationend', this);
27343 }
27344 },
27345
27346 applyInAnimation: function(animation, inAnimation) {
27347 return Ext.factory(animation, Ext.fx.Animation, inAnimation);
27348 },
27349
27350 applyOutAnimation: function(animation, outAnimation) {
27351 return Ext.factory(animation, Ext.fx.Animation, outAnimation);
27352 },
27353
27354 updateInAnimation: function(animation) {
27355 animation.setScope(this);
27356 },
27357
27358 updateOutAnimation: function(animation) {
27359 animation.setScope(this);
27360 },
27361
27362 onActiveItemChange: function(cardLayout, newItem, oldItem, options, controller) {
27363 var inAnimation = this.getInAnimation(),
27364 outAnimation = this.getOutAnimation(),
27365 inElement, outElement;
27366
27367 if (newItem && oldItem && oldItem.isPainted()) {
27368 inElement = newItem.renderElement;
27369 outElement = oldItem.renderElement;
27370
27371 inAnimation.setElement(inElement);
27372 outAnimation.setElement(outElement);
27373
27374 outAnimation.setOnBeforeEnd(function(element, interrupted) {
27375 if (interrupted || Ext.Animator.hasRunningAnimations(element)) {
27376 controller.firingArguments[1] = null;
27377 controller.firingArguments[2] = null;
27378 }
27379 });
27380 outAnimation.setOnEnd(function() {
27381 controller.resume();
27382 });
27383
27384 inElement.dom.style.setProperty('visibility', 'hidden', 'important');
27385 newItem.show();
27386
27387 Ext.Animator.run([outAnimation, inAnimation]);
27388 controller.pause();
27389 }
27390 },
27391
27392 destroy: function () {
27393 Ext.destroy(this.getInAnimation(), this.getOutAnimation());
27394
27395 this.callParent(arguments);
27396 }
27397 });
27398
27399 /**
27400 * @private
27401 */
27402 Ext.define('Ext.fx.layout.card.Slide', {
27403 extend: Ext.fx.layout.card.Style ,
27404
27405 alias: 'fx.layout.card.slide',
27406
27407 config: {
27408 inAnimation: {
27409 type: 'slide',
27410 easing: 'ease-out'
27411 },
27412 outAnimation: {
27413 type: 'slide',
27414 easing: 'ease-out',
27415 out: true
27416 }
27417 },
27418
27419 updateReverse: function(reverse) {
27420 this.getInAnimation().setReverse(reverse);
27421 this.getOutAnimation().setReverse(reverse);
27422 }
27423 });
27424
27425 /**
27426 * @private
27427 */
27428 Ext.define('Ext.fx.layout.card.Cover', {
27429 extend: Ext.fx.layout.card.Style ,
27430
27431 alias: 'fx.layout.card.cover',
27432
27433 config: {
27434 reverse: null,
27435
27436 inAnimation: {
27437 before: {
27438 'z-index': 100
27439 },
27440 after: {
27441 'z-index': 0
27442 },
27443 type: 'slide',
27444 easing: 'ease-out'
27445 },
27446 outAnimation: {
27447 easing: 'ease-out',
27448 from: {
27449 opacity: 0.99
27450 },
27451 to: {
27452 opacity: 1
27453 },
27454 out: true
27455 }
27456 },
27457
27458 updateReverse: function(reverse) {
27459 this.getInAnimation().setReverse(reverse);
27460 this.getOutAnimation().setReverse(reverse);
27461 }
27462 });
27463
27464 /**
27465 * @private
27466 */
27467 Ext.define('Ext.fx.layout.card.Reveal', {
27468 extend: Ext.fx.layout.card.Style ,
27469
27470 alias: 'fx.layout.card.reveal',
27471
27472 config: {
27473 inAnimation: {
27474 easing: 'ease-out',
27475 from: {
27476 opacity: 0.99
27477 },
27478 to: {
27479 opacity: 1
27480 }
27481 },
27482 outAnimation: {
27483 before: {
27484 'z-index': 100
27485 },
27486 after: {
27487 'z-index': 0
27488 },
27489 type: 'slide',
27490 easing: 'ease-out',
27491 out: true
27492 }
27493 },
27494
27495 updateReverse: function(reverse) {
27496 this.getInAnimation().setReverse(reverse);
27497 this.getOutAnimation().setReverse(reverse);
27498 }
27499 });
27500
27501 /**
27502 * @private
27503 */
27504 Ext.define('Ext.fx.layout.card.Fade', {
27505 extend: Ext.fx.layout.card.Style ,
27506
27507 alias: 'fx.layout.card.fade',
27508
27509 config: {
27510 reverse: null,
27511
27512 inAnimation: {
27513 type: 'fade',
27514 easing: 'ease-out'
27515 },
27516 outAnimation: {
27517 type: 'fade',
27518 easing: 'ease-out',
27519 out: true
27520 }
27521 }
27522 });
27523
27524 /**
27525 * @private
27526 */
27527 Ext.define('Ext.fx.layout.card.Flip', {
27528 extend: Ext.fx.layout.card.Style ,
27529
27530 alias: 'fx.layout.card.flip',
27531
27532 config: {
27533 duration: 500,
27534
27535 inAnimation: {
27536 type: 'flip',
27537 half: true,
27538 easing: 'ease-out',
27539 before: {
27540 'backface-visibility': 'hidden'
27541 },
27542 after: {
27543 'backface-visibility': null
27544 }
27545 },
27546 outAnimation: {
27547 type: 'flip',
27548 half: true,
27549 easing: 'ease-in',
27550 before: {
27551 'backface-visibility': 'hidden'
27552 },
27553 after: {
27554 'backface-visibility': null
27555 },
27556 out: true
27557 }
27558 },
27559
27560 onActiveItemChange: function(cardLayout, newItem, oldItem, options, controller) {
27561 var parent = newItem.element.getParent();
27562 parent.addCls('x-layout-card-perspective');
27563
27564 this.on('animationend', function() {
27565 parent.removeCls('x-layout-card-perspective');
27566 }, this, {single: true});
27567
27568 this.callParent(arguments);
27569 },
27570
27571 updateDuration: function(duration) {
27572 var halfDuration = duration / 2,
27573 inAnimation = this.getInAnimation(),
27574 outAnimation = this.getOutAnimation();
27575
27576 inAnimation.setDelay(halfDuration);
27577 inAnimation.setDuration(halfDuration);
27578 outAnimation.setDuration(halfDuration);
27579 }
27580 });
27581
27582 /**
27583 * @private
27584 */
27585 Ext.define('Ext.fx.layout.card.Pop', {
27586 extend: Ext.fx.layout.card.Style ,
27587
27588 alias: 'fx.layout.card.pop',
27589
27590 config: {
27591 duration: 500,
27592
27593 inAnimation: {
27594 type: 'pop',
27595 easing: 'ease-out'
27596 },
27597 outAnimation: {
27598 type: 'pop',
27599 easing: 'ease-in',
27600 out: true
27601 }
27602 },
27603
27604 updateDuration: function(duration) {
27605 var halfDuration = duration / 2,
27606 inAnimation = this.getInAnimation(),
27607 outAnimation = this.getOutAnimation();
27608
27609 inAnimation.setDelay(halfDuration);
27610 inAnimation.setDuration(halfDuration);
27611 outAnimation.setDuration(halfDuration);
27612 }
27613 });
27614
27615 /**
27616 * @private
27617 */
27618 Ext.define('Ext.fx.layout.card.Scroll', {
27619 extend: Ext.fx.layout.card.Abstract ,
27620
27621
27622
27623
27624
27625 alias: 'fx.layout.card.scroll',
27626
27627 config: {
27628 duration: 150
27629 },
27630
27631 constructor: function(config) {
27632 this.initConfig(config);
27633 },
27634
27635 getEasing: function() {
27636 var easing = this.easing;
27637
27638 if (!easing) {
27639 this.easing = easing = new Ext.fx.easing.Linear();
27640 }
27641
27642 return easing;
27643 },
27644
27645 updateDuration: function(duration) {
27646 this.getEasing().setDuration(duration);
27647 },
27648
27649 onActiveItemChange: function(cardLayout, newItem, oldItem, options, controller) {
27650 var direction = this.getDirection(),
27651 easing = this.getEasing(),
27652 containerElement, inElement, outElement, containerWidth, containerHeight, reverse;
27653
27654 if (newItem && oldItem) {
27655 if (this.isAnimating) {
27656 this.stopAnimation();
27657 }
27658
27659 newItem.setWidth('100%');
27660 newItem.setHeight('100%');
27661
27662 containerElement = this.getLayout().container.innerElement;
27663 containerWidth = containerElement.getWidth();
27664 containerHeight = containerElement.getHeight();
27665
27666 inElement = newItem.renderElement;
27667 outElement = oldItem.renderElement;
27668
27669 this.oldItem = oldItem;
27670 this.newItem = newItem;
27671 this.currentEventController = controller;
27672 this.containerElement = containerElement;
27673 this.isReverse = reverse = this.getReverse();
27674
27675 newItem.show();
27676
27677 if (direction == 'right') {
27678 direction = 'left';
27679 this.isReverse = reverse = !reverse;
27680 }
27681 else if (direction == 'down') {
27682 direction = 'up';
27683 this.isReverse = reverse = !reverse;
27684 }
27685
27686 if (direction == 'left') {
27687 if (reverse) {
27688 easing.setConfig({
27689 startValue: containerWidth,
27690 endValue: 0
27691 });
27692
27693 containerElement.dom.scrollLeft = containerWidth;
27694 outElement.setLeft(containerWidth);
27695 }
27696 else {
27697 easing.setConfig({
27698 startValue: 0,
27699 endValue: containerWidth
27700 });
27701
27702 inElement.setLeft(containerWidth);
27703 }
27704 }
27705 else {
27706 if (reverse) {
27707 easing.setConfig({
27708 startValue: containerHeight,
27709 endValue: 0
27710 });
27711
27712 containerElement.dom.scrollTop = containerHeight;
27713 outElement.setTop(containerHeight);
27714 }
27715 else {
27716 easing.setConfig({
27717 startValue: 0,
27718 endValue: containerHeight
27719 });
27720
27721 inElement.setTop(containerHeight);
27722 }
27723 }
27724
27725 this.startAnimation();
27726
27727 controller.pause();
27728 }
27729 },
27730
27731 startAnimation: function() {
27732 this.isAnimating = true;
27733 this.getEasing().setStartTime(Date.now());
27734 Ext.AnimationQueue.start(this.doAnimationFrame, this);
27735 },
27736
27737 doAnimationFrame: function() {
27738 var easing = this.getEasing(),
27739 direction = this.getDirection(),
27740 scroll = 'scrollTop',
27741 value;
27742
27743 if (direction == 'left' || direction == 'right') {
27744 scroll = 'scrollLeft';
27745 }
27746
27747 if (easing.isEnded) {
27748 this.stopAnimation();
27749 }
27750 else {
27751 value = easing.getValue();
27752 this.containerElement.dom[scroll] = value;
27753 }
27754 },
27755
27756 stopAnimation: function() {
27757 var me = this,
27758 direction = me.getDirection(),
27759 scroll = 'setTop',
27760 oldItem = me.oldItem,
27761 newItem = me.newItem;
27762
27763 if (direction == 'left' || direction == 'right') {
27764 scroll = 'setLeft';
27765 }
27766
27767 me.currentEventController.resume();
27768
27769 if (me.isReverse && oldItem && oldItem.renderElement && oldItem.renderElement.dom) {
27770 oldItem.renderElement[scroll](null);
27771 }
27772 else if (newItem && newItem.renderElement && newItem.renderElement.dom) {
27773 newItem.renderElement[scroll](null);
27774 }
27775
27776 Ext.AnimationQueue.stop(this.doAnimationFrame, this);
27777 me.isAnimating = false;
27778 me.fireEvent('animationend', me);
27779 }
27780 });
27781
27782 /**
27783 * @private
27784 */
27785 Ext.define('Ext.fx.layout.Card', {
27786
27787
27788
27789
27790
27791
27792
27793
27794
27795
27796
27797 constructor: function(config) {
27798 var defaultClass = Ext.fx.layout.card.Abstract,
27799 type;
27800
27801 if (!config) {
27802 return null;
27803 }
27804
27805 if (typeof config == 'string') {
27806 type = config;
27807
27808 config = {};
27809 }
27810 else if (config.type) {
27811 type = config.type;
27812 }
27813
27814 config.elementBox = false;
27815
27816 if (type) {
27817 if (Ext.browser.is.AndroidStock2) {
27818 // In Android 2 we only support scroll and fade. Otherwise force it to slide.
27819 if (type != 'fade') {
27820 type = 'scroll';
27821 }
27822 }
27823
27824 defaultClass = Ext.ClassManager.getByAlias('fx.layout.card.' + type);
27825
27826 //<debug error>
27827 if (!defaultClass) {
27828 Ext.Logger.error("Unknown card animation type: '" + type + "'");
27829 }
27830 //</debug>
27831 }
27832
27833 return Ext.factory(config, defaultClass);
27834 }
27835 });
27836
27837 /**
27838 * Sometimes you want to show several screens worth of information but you've only got a small screen to work with.
27839 * TabPanels and Carousels both enable you to see one screen of many at a time, and underneath they both use a Card
27840 * Layout.
27841 *
27842 * Card Layout takes the size of the Container it is applied to and sizes the currently active item to fill the
27843 * Container completely. It then hides the rest of the items, allowing you to change which one is currently visible but
27844 * only showing one at once:
27845 *
27846 * {@img ../guides/layouts/card.jpg}
27847 *
27848 *
27849 * Here the gray box is our Container, and the blue box inside it is the currently active card. The three other cards
27850 * are hidden from view, but can be swapped in later. While it's not too common to create Card layouts directly, you
27851 * can do so like this:
27852 *
27853 * var panel = Ext.create('Ext.Panel', {
27854 * layout: 'card',
27855 * items: [
27856 * {
27857 * html: "First Item"
27858 * },
27859 * {
27860 * html: "Second Item"
27861 * },
27862 * {
27863 * html: "Third Item"
27864 * },
27865 * {
27866 * html: "Fourth Item"
27867 * }
27868 * ]
27869 * });
27870 *
27871 * panel.{@link Ext.Container#setActiveItem setActiveItem}(1);
27872 *
27873 * Here we create a Panel with a Card Layout and later set the second item active (the active item index is zero-based,
27874 * so 1 corresponds to the second item). Normally you're better off using a {@link Ext.tab.Panel tab panel} or a
27875 * {@link Ext.carousel.Carousel carousel}.
27876 *
27877 * For a more detailed overview of Sencha Touch 2 layouts, check out the
27878 * [Layout Guide](../../../core_concepts/layouts.html).
27879 */
27880
27881
27882 Ext.define('Ext.layout.Card', {
27883 extend: Ext.layout.Default ,
27884
27885 alias: 'layout.card',
27886
27887 isCard: true,
27888
27889 /**
27890 * @event activeitemchange
27891 * @preventable doActiveItemChange
27892 * Fires when an card is made active
27893 * @param {Ext.layout.Card} this The layout instance
27894 * @param {Mixed} newActiveItem The new active item
27895 * @param {Mixed} oldActiveItem The old active item
27896 */
27897
27898 layoutClass: 'x-layout-card',
27899
27900 itemClass: 'x-layout-card-item',
27901
27902
27903
27904
27905
27906 /**
27907 * @private
27908 */
27909 applyAnimation: function(animation) {
27910 return new Ext.fx.layout.Card(animation);
27911 },
27912
27913 /**
27914 * @private
27915 */
27916 updateAnimation: function(animation, oldAnimation) {
27917 if (animation && animation.isAnimation) {
27918 animation.setLayout(this);
27919 }
27920
27921 if (oldAnimation) {
27922 oldAnimation.destroy();
27923 }
27924 },
27925
27926 setContainer: function(container) {
27927 this.callSuper(arguments);
27928
27929 container.innerElement.addCls(this.layoutClass);
27930 container.onInitialized('onContainerInitialized', this);
27931 },
27932
27933 onContainerInitialized: function() {
27934 var container = this.container,
27935 firstItem = container.getInnerAt(0),
27936 activeItem = container.getActiveItem();
27937
27938 if (activeItem) {
27939 activeItem.show();
27940 if(firstItem && firstItem !== activeItem) {
27941 firstItem.hide();
27942 }
27943 }
27944
27945 container.on('activeitemchange', 'onContainerActiveItemChange', this);
27946 },
27947
27948 /**
27949 * @private
27950 */
27951 onContainerActiveItemChange: function(container) {
27952 this.relayEvent(arguments, 'doActiveItemChange');
27953 },
27954
27955 onItemInnerStateChange: function(item, isInner, destroying) {
27956 this.callSuper(arguments);
27957 var container = this.container,
27958 activeItem = container.getActiveItem();
27959
27960 item.toggleCls(this.itemClass, isInner);
27961 item.setLayoutSizeFlags(isInner ? container.LAYOUT_BOTH : 0);
27962
27963 if (isInner) {
27964 if (activeItem !== container.innerIndexOf(item) && activeItem !== item && item !== container.pendingActiveItem) {
27965 item.hide();
27966 }
27967 }
27968 else {
27969 if (!destroying && !item.isDestroyed && item.isDestroying !== true) {
27970 item.show();
27971 }
27972 }
27973 },
27974
27975 /**
27976 * @private
27977 */
27978 doActiveItemChange: function(me, newActiveItem, oldActiveItem) {
27979 if (oldActiveItem) {
27980 oldActiveItem.hide();
27981 }
27982
27983 if (newActiveItem) {
27984 newActiveItem.show();
27985 }
27986 },
27987
27988 destroy: function () {
27989 this.callParent(arguments);
27990 Ext.destroy(this.getAnimation());
27991 }
27992 });
27993
27994 /**
27995 * Represents a filter that can be applied to a {@link Ext.util.MixedCollection MixedCollection}. Can either simply
27996 * filter on a property/value pair or pass in a filter function with custom logic. Filters are always used in the
27997 * context of MixedCollections, though {@link Ext.data.Store Store}s frequently create them when filtering and searching
27998 * on their records. Example usage:
27999 *
28000 * // Set up a fictional MixedCollection containing a few people to filter on
28001 * var allNames = new Ext.util.MixedCollection();
28002 * allNames.addAll([
28003 * { id: 1, name: 'Ed', age: 25 },
28004 * { id: 2, name: 'Jamie', age: 37 },
28005 * { id: 3, name: 'Abe', age: 32 },
28006 * { id: 4, name: 'Aaron', age: 26 },
28007 * { id: 5, name: 'David', age: 32 }
28008 * ]);
28009 *
28010 * var ageFilter = new Ext.util.Filter({
28011 * property: 'age',
28012 * value : 32
28013 * });
28014 *
28015 * var longNameFilter = new Ext.util.Filter({
28016 * filterFn: function(item) {
28017 * return item.name.length > 4;
28018 * }
28019 * });
28020 *
28021 * // a new MixedCollection with the 3 names longer than 4 characters
28022 * var longNames = allNames.filter(longNameFilter);
28023 *
28024 * // a new MixedCollection with the 2 people of age 32:
28025 * var youngFolk = allNames.filter(ageFilter);
28026 */
28027 Ext.define('Ext.util.Filter', {
28028 isFilter: true,
28029
28030 config: {
28031 /**
28032 * @cfg {String} [property=null]
28033 * The property to filter on. Required unless a `filter` is passed
28034 */
28035 property: null,
28036
28037 /**
28038 * @cfg {RegExp/Mixed} [value=null]
28039 * The value you want to match against. Can be a regular expression which will be used as matcher or any other
28040 * value. Mixed can be an object or an array of objects.
28041 */
28042 value: null,
28043
28044 /**
28045 * @cfg {Function} filterFn
28046 * A custom filter function which is passed each item in the {@link Ext.util.MixedCollection} in turn. Should
28047 * return true to accept each item or false to reject it
28048 */
28049 filterFn: Ext.emptyFn,
28050
28051 /**
28052 * @cfg {Boolean} [anyMatch=false]
28053 * True to allow any match - no regex start/end line anchors will be added.
28054 */
28055 anyMatch: false,
28056
28057 /**
28058 * @cfg {Boolean} [exactMatch=false]
28059 * True to force exact match (^ and $ characters added to the regex). Ignored if anyMatch is true.
28060 */
28061 exactMatch: false,
28062
28063 /**
28064 * @cfg {Boolean} [caseSensitive=false]
28065 * True to make the regex case sensitive (adds 'i' switch to regex).
28066 */
28067 caseSensitive: false,
28068
28069 /**
28070 * @cfg {String} [root=null]
28071 * Optional root property. This is mostly useful when filtering a Store, in which case we set the root to 'data'
28072 * to make the filter pull the {@link #property} out of the data object of each item
28073 */
28074 root: null,
28075
28076 /**
28077 * @cfg {String} id
28078 * An optional id this filter can be keyed by in Collections. If no id is specified it will generate an id by
28079 * first trying a combination of property-value, and if none if these were specified (like when having a
28080 * filterFn) it will generate a random id.
28081 */
28082 id: undefined,
28083
28084 /**
28085 * @cfg {Object} [scope=null]
28086 * The scope in which to run the filterFn
28087 */
28088 scope: null
28089 },
28090
28091 applyId: function(id) {
28092 if (!id) {
28093 if (this.getProperty()) {
28094 id = this.getProperty() + '-' + String(this.getValue());
28095 }
28096 if (!id) {
28097 id = Ext.id(null, 'ext-filter-');
28098 }
28099 }
28100
28101 return id;
28102 },
28103
28104 /**
28105 * Creates new Filter.
28106 * @param {Object} config Config object
28107 */
28108 constructor: function(config) {
28109 this.initConfig(config);
28110 },
28111
28112 applyFilterFn: function(filterFn) {
28113 if (filterFn === Ext.emptyFn) {
28114 filterFn = this.getInitialConfig('filter');
28115 if (filterFn) {
28116 return filterFn;
28117 }
28118
28119 var value = this.getValue();
28120 if (!this.getProperty() && !value && value !== 0) {
28121 // <debug>
28122 Ext.Logger.error('A Filter requires either a property and value, or a filterFn to be set');
28123 // </debug>
28124 return Ext.emptyFn;
28125 }
28126 else {
28127 return this.createFilterFn();
28128 }
28129 }
28130 return filterFn;
28131 },
28132
28133 /**
28134 * @private
28135 * Creates a filter function for the configured property/value/anyMatch/caseSensitive options for this Filter
28136 */
28137 createFilterFn: function() {
28138 var me = this,
28139 matcher = me.createValueMatcher();
28140
28141 return function(item) {
28142 var root = me.getRoot(),
28143 property = me.getProperty();
28144
28145 if (root) {
28146 item = item[root];
28147 }
28148
28149 return matcher.test(item[property]);
28150 };
28151 },
28152
28153 /**
28154 * @private
28155 * Returns a regular expression based on the given value and matching options
28156 */
28157 createValueMatcher: function() {
28158 var me = this,
28159 value = me.getValue(),
28160 anyMatch = me.getAnyMatch(),
28161 exactMatch = me.getExactMatch(),
28162 caseSensitive = me.getCaseSensitive(),
28163 escapeRe = Ext.String.escapeRegex;
28164
28165 if (value === null || value === undefined || !value.exec) { // not a regex
28166 value = String(value);
28167
28168 if (anyMatch === true) {
28169 value = escapeRe(value);
28170 } else {
28171 value = '^' + escapeRe(value);
28172 if (exactMatch === true) {
28173 value += '$';
28174 }
28175 }
28176 value = new RegExp(value, caseSensitive ? '' : 'i');
28177 }
28178
28179 return value;
28180 }
28181 });
28182
28183 /**
28184 * @private
28185 */
28186 Ext.define('Ext.util.AbstractMixedCollection', {
28187
28188
28189 mixins: {
28190 observable: Ext.mixin.Observable
28191 },
28192
28193 /**
28194 * @event clear
28195 * Fires when the collection is cleared.
28196 */
28197
28198 /**
28199 * @event add
28200 * Fires when an item is added to the collection.
28201 * @param {Number} index The index at which the item was added.
28202 * @param {Object} o The item added.
28203 * @param {String} key The key associated with the added item.
28204 */
28205
28206 /**
28207 * @event replace
28208 * Fires when an item is replaced in the collection.
28209 * @param {String} key he key associated with the new added.
28210 * @param {Object} old The item being replaced.
28211 * @param {Object} new The new item.
28212 */
28213
28214 /**
28215 * @event remove
28216 * Fires when an item is removed from the collection.
28217 * @param {Object} o The item being removed.
28218 * @param {String} key (optional) The key associated with the removed item.
28219 */
28220
28221 /**
28222 * Creates new MixedCollection.
28223 * @param {Boolean} [allowFunctions=false] Specify `true` if the {@link #addAll}
28224 * function should add function references to the collection.
28225 * @param {Function} [keyFn] A function that can accept an item of the type(s) stored in this MixedCollection
28226 * and return the key value for that item. This is used when available to look up the key on items that
28227 * were passed without an explicit key parameter to a MixedCollection method. Passing this parameter is
28228 * equivalent to providing an implementation for the {@link #getKey} method.
28229 */
28230 constructor: function(allowFunctions, keyFn) {
28231 var me = this;
28232
28233 me.items = [];
28234 me.map = {};
28235 me.keys = [];
28236 me.length = 0;
28237
28238 me.allowFunctions = allowFunctions === true;
28239
28240 if (keyFn) {
28241 me.getKey = keyFn;
28242 }
28243
28244 me.mixins.observable.constructor.call(me);
28245 },
28246
28247 /**
28248 * @cfg {Boolean} allowFunctions Specify `true` if the {@link #addAll}
28249 * function should add function references to the collection.
28250 */
28251 allowFunctions : false,
28252
28253 /**
28254 * Adds an item to the collection. Fires the {@link #event-add} event when complete.
28255 * @param {String} key The key to associate with the item, or the new item.
28256 *
28257 * If a {@link #getKey} implementation was specified for this MixedCollection,
28258 * or if the key of the stored items is in a property called `id`,
28259 * the MixedCollection will be able to _derive_ the key for the new item.
28260 * In this case just pass the new item in this parameter.
28261 * @param {Object} obj The item to add.
28262 * @return {Object} The item added.
28263 */
28264 add: function(key, obj){
28265 var me = this,
28266 myObj = obj,
28267 myKey = key,
28268 old;
28269
28270 if (arguments.length == 1) {
28271 myObj = myKey;
28272 myKey = me.getKey(myObj);
28273 }
28274 if (typeof myKey != 'undefined' && myKey !== null) {
28275 old = me.map[myKey];
28276 if (typeof old != 'undefined') {
28277 return me.replace(myKey, myObj);
28278 }
28279 me.map[myKey] = myObj;
28280 }
28281 me.length++;
28282 me.items.push(myObj);
28283 me.keys.push(myKey);
28284 me.fireEvent('add', me.length - 1, myObj, myKey);
28285 return myObj;
28286 },
28287
28288 /**
28289 * MixedCollection has a generic way to fetch keys if you implement `getKey`. The default implementation
28290 * simply returns `item.id` but you can provide your own implementation
28291 * to return a different value as in the following examples:
28292 *
28293 * // normal way
28294 * var mc = new Ext.util.MixedCollection();
28295 * mc.add(someEl.dom.id, someEl);
28296 * mc.add(otherEl.dom.id, otherEl);
28297 * //and so on
28298 *
28299 * // using getKey
28300 * var mc = new Ext.util.MixedCollection();
28301 * mc.getKey = function(el) {
28302 * return el.dom.id;
28303 * };
28304 * mc.add(someEl);
28305 * mc.add(otherEl);
28306 *
28307 * // or via the constructor
28308 * var mc = new Ext.util.MixedCollection(false, function(el) {
28309 * return el.dom.id;
28310 * });
28311 * mc.add(someEl);
28312 * mc.add(otherEl);
28313 *
28314 * @param {Object} item The item for which to find the key.
28315 * @return {Object} The key for the passed item.
28316 */
28317 getKey: function(o){
28318 return o.id;
28319 },
28320
28321 /**
28322 * Replaces an item in the collection. Fires the {@link #event-replace} event when complete.
28323 * @param {String} key The key associated with the item to replace, or the replacement item.
28324 *
28325 * If you supplied a {@link #getKey} implementation for this MixedCollection, or if the key
28326 * of your stored items is in a property called `id`, then the MixedCollection
28327 * will be able to _derive_ the key of the replacement item. If you want to replace an item
28328 * with one having the same key value, then just pass the replacement item in this parameter.
28329 * @param {Object} o (optional) If the first parameter passed was a key, the item to associate
28330 * with that key.
28331 * @return {Object} The new item.
28332 */
28333 replace: function(key, o){
28334 var me = this,
28335 old,
28336 index;
28337
28338 if (arguments.length == 1) {
28339 o = arguments[0];
28340 key = me.getKey(o);
28341 }
28342 old = me.map[key];
28343 if (typeof key == 'undefined' || key === null || typeof old == 'undefined') {
28344 return me.add(key, o);
28345 }
28346 index = me.indexOfKey(key);
28347 me.items[index] = o;
28348 me.map[key] = o;
28349 me.fireEvent('replace', key, old, o);
28350 return o;
28351 },
28352
28353 /**
28354 * Adds all elements of an Array or an Object to the collection.
28355 * @param {Object/Array} objs An Object containing properties which will be added
28356 * to the collection, or an Array of values, each of which are added to the collection.
28357 * Functions references will be added to the collection if `{@link #allowFunctions}`
28358 * has been set to `true`.
28359 */
28360 addAll: function(objs){
28361 var me = this,
28362 i = 0,
28363 args,
28364 len,
28365 key;
28366
28367 if (arguments.length > 1 || Ext.isArray(objs)) {
28368 args = arguments.length > 1 ? arguments : objs;
28369 for (len = args.length; i < len; i++) {
28370 me.add(args[i]);
28371 }
28372 } else {
28373 for (key in objs) {
28374 if (objs.hasOwnProperty(key)) {
28375 if (me.allowFunctions || typeof objs[key] != 'function') {
28376 me.add(key, objs[key]);
28377 }
28378 }
28379 }
28380 }
28381 },
28382
28383 /**
28384 * Executes the specified function once for every item in the collection.
28385 *
28386 * @param {Function} fn The function to execute for each item.
28387 * @param {Mixed} fn.item The collection item.
28388 * @param {Number} fn.index The item's index.
28389 * @param {Number} fn.length The total number of items in the collection.
28390 * @param {Boolean} fn.return Returning `false` will stop the iteration.
28391 * @param {Object} scope (optional) The scope (`this` reference) in which the function is executed.
28392 * Defaults to the current item in the iteration.
28393 */
28394 each: function(fn, scope){
28395 var items = [].concat(this.items), // each safe for removal
28396 i = 0,
28397 len = items.length,
28398 item;
28399
28400 for (; i < len; i++) {
28401 item = items[i];
28402 if (fn.call(scope || item, item, i, len) === false) {
28403 break;
28404 }
28405 }
28406 },
28407
28408 /**
28409 * Executes the specified function once for every key in the collection, passing each
28410 * key, and its associated item as the first two parameters.
28411 * @param {Function} fn The function to execute for each item.
28412 * @param {Object} scope (optional) The scope (`this` reference) in which the function is executed. Defaults to the browser window.
28413 */
28414 eachKey: function(fn, scope){
28415 var keys = this.keys,
28416 items = this.items,
28417 i = 0,
28418 len = keys.length;
28419
28420 for (; i < len; i++) {
28421 fn.call(scope || window, keys[i], items[i], i, len);
28422 }
28423 },
28424
28425 /**
28426 * Returns the first item in the collection which elicits a `true` return value from the
28427 * passed selection function.
28428 * @param {Function} fn The selection function to execute for each item.
28429 * @param {Object} scope (optional) The scope (`this` reference) in which the function is executed. Defaults to the browser window.
28430 * @return {Object} The first item in the collection which returned `true` from the selection function.
28431 */
28432 findBy: function(fn, scope) {
28433 var keys = this.keys,
28434 items = this.items,
28435 i = 0,
28436 len = items.length;
28437
28438 for (; i < len; i++) {
28439 if (fn.call(scope || window, items[i], keys[i])) {
28440 return items[i];
28441 }
28442 }
28443 return null;
28444 },
28445
28446 /**
28447 * Inserts an item at the specified index in the collection. Fires the `{@link #event-add}` event when complete.
28448 * @param {Number} index The index to insert the item at.
28449 * @param {String} key The key to associate with the new item, or the item itself.
28450 * @param {Object} [obj] If the second parameter was a key, the new item.
28451 * @return {Object} The item inserted.
28452 */
28453 insert: function(index, key, obj){
28454 var me = this,
28455 myKey = key,
28456 myObj = obj;
28457
28458 if (arguments.length == 2) {
28459 myObj = myKey;
28460 myKey = me.getKey(myObj);
28461 }
28462 if (me.containsKey(myKey)) {
28463 me.suspendEvents();
28464 me.removeAtKey(myKey);
28465 me.resumeEvents();
28466 }
28467 if (index >= me.length) {
28468 return me.add(myKey, myObj);
28469 }
28470 me.length++;
28471 Ext.Array.splice(me.items, index, 0, myObj);
28472 if (typeof myKey != 'undefined' && myKey !== null) {
28473 me.map[myKey] = myObj;
28474 }
28475 Ext.Array.splice(me.keys, index, 0, myKey);
28476 me.fireEvent('add', index, myObj, myKey);
28477 return myObj;
28478 },
28479
28480 /**
28481 * Remove an item from the collection.
28482 * @param {Object} o The item to remove.
28483 * @return {Object} The item removed or `false` if no item was removed.
28484 */
28485 remove: function(o){
28486 return this.removeAt(this.indexOf(o));
28487 },
28488
28489 /**
28490 * Remove all items in the passed array from the collection.
28491 * @param {Array} items An array of items to be removed.
28492 * @return {Ext.util.MixedCollection} this object
28493 */
28494 removeAll: function(items){
28495 Ext.each(items || [], function(item) {
28496 this.remove(item);
28497 }, this);
28498
28499 return this;
28500 },
28501
28502 /**
28503 * Remove an item from a specified index in the collection. Fires the `{@link #event-remove}` event when complete.
28504 * @param {Number} index The index within the collection of the item to remove.
28505 * @return {Object/Boolean} The item removed or `false` if no item was removed.
28506 */
28507 removeAt: function(index){
28508 var me = this,
28509 o,
28510 key;
28511
28512 if (index < me.length && index >= 0) {
28513 me.length--;
28514 o = me.items[index];
28515 Ext.Array.erase(me.items, index, 1);
28516 key = me.keys[index];
28517 if (typeof key != 'undefined') {
28518 delete me.map[key];
28519 }
28520 Ext.Array.erase(me.keys, index, 1);
28521 me.fireEvent('remove', o, key);
28522 return o;
28523 }
28524 return false;
28525 },
28526
28527 /**
28528 * Removed an item associated with the passed key from the collection.
28529 * @param {String} key The key of the item to remove.
28530 * @return {Object/Boolean} The item removed or `false` if no item was removed.
28531 */
28532 removeAtKey: function(key){
28533 return this.removeAt(this.indexOfKey(key));
28534 },
28535
28536 /**
28537 * Returns the number of items in the collection.
28538 * @return {Number} the number of items in the collection.
28539 */
28540 getCount: function(){
28541 return this.length;
28542 },
28543
28544 /**
28545 * Returns index within the collection of the passed Object.
28546 * @param {Object} o The item to find the index of.
28547 * @return {Number} index of the item. Returns -1 if not found.
28548 */
28549 indexOf: function(o){
28550 return Ext.Array.indexOf(this.items, o);
28551 },
28552
28553 /**
28554 * Returns index within the collection of the passed key.
28555 * @param {String} key The key to find the index of.
28556 * @return {Number} The index of the key.
28557 */
28558 indexOfKey: function(key){
28559 return Ext.Array.indexOf(this.keys, key);
28560 },
28561
28562 /**
28563 * Returns the item associated with the passed key OR index.
28564 * Key has priority over index. This is the equivalent
28565 * of calling {@link #getByKey} first, then if nothing matched calling {@link #getAt}.
28566 * @param {String/Number} key The key or index of the item.
28567 * @return {Object} If the item is found, returns the item. If the item was not found, returns `undefined`.
28568 * If an item was found, but is a Class, returns `null`.
28569 */
28570 get: function(key) {
28571 var me = this,
28572 mk = me.map[key],
28573 item = mk !== undefined ? mk : (typeof key == 'number') ? me.items[key] : undefined;
28574 return typeof item != 'function' || me.allowFunctions ? item : null; // for prototype!
28575 },
28576
28577 /**
28578 * Returns the item at the specified index.
28579 * @param {Number} index The index of the item.
28580 * @return {Object} The item at the specified index.
28581 */
28582 getAt: function(index) {
28583 return this.items[index];
28584 },
28585
28586 /**
28587 * Returns the item associated with the passed key.
28588 * @param {String/Number} key The key of the item.
28589 * @return {Object} The item associated with the passed key.
28590 */
28591 getByKey: function(key) {
28592 return this.map[key];
28593 },
28594
28595 /**
28596 * Returns `true` if the collection contains the passed Object as an item.
28597 * @param {Object} o The Object to look for in the collection.
28598 * @return {Boolean} `true` if the collection contains the Object as an item.
28599 */
28600 contains: function(o){
28601 return Ext.Array.contains(this.items, o);
28602 },
28603
28604 /**
28605 * Returns `true` if the collection contains the passed Object as a key.
28606 * @param {String} key The key to look for in the collection.
28607 * @return {Boolean} `true` if the collection contains the Object as a key.
28608 */
28609 containsKey: function(key){
28610 return typeof this.map[key] != 'undefined';
28611 },
28612
28613 /**
28614 * Removes all items from the collection. Fires the `{@link #event-clear}` event when complete.
28615 */
28616 clear: function(){
28617 var me = this;
28618
28619 me.length = 0;
28620 me.items = [];
28621 me.keys = [];
28622 me.map = {};
28623 me.fireEvent('clear');
28624 },
28625
28626 /**
28627 * Returns the first item in the collection.
28628 * @return {Object} the first item in the collection..
28629 */
28630 first: function() {
28631 return this.items[0];
28632 },
28633
28634 /**
28635 * Returns the last item in the collection.
28636 * @return {Object} the last item in the collection..
28637 */
28638 last: function() {
28639 return this.items[this.length - 1];
28640 },
28641
28642 /**
28643 * Collects all of the values of the given property and returns their sum.
28644 * @param {String} property The property to sum by.
28645 * @param {String} [root] Optional 'root' property to extract the first argument from. This is used mainly when
28646 * summing fields in records, where the fields are all stored inside the `data` object
28647 * @param {Number} [start=0] (optional) The record index to start at.
28648 * @param {Number} [end=-1] (optional) The record index to end at.
28649 * @return {Number} The total
28650 */
28651 sum: function(property, root, start, end) {
28652 var values = this.extractValues(property, root),
28653 length = values.length,
28654 sum = 0,
28655 i;
28656
28657 start = start || 0;
28658 end = (end || end === 0) ? end : length - 1;
28659
28660 for (i = start; i <= end; i++) {
28661 sum += values[i];
28662 }
28663
28664 return sum;
28665 },
28666
28667 /**
28668 * Collects unique values of a particular property in this MixedCollection.
28669 * @param {String} property The property to collect on.
28670 * @param {String} [root] Optional 'root' property to extract the first argument from. This is used mainly when
28671 * summing fields in records, where the fields are all stored inside the `data` object.
28672 * @param {Boolean} [allowNull] Pass `true` to allow `null`, `undefined`, or empty string values.
28673 * @return {Array} The unique values.
28674 */
28675 collect: function(property, root, allowNull) {
28676 var values = this.extractValues(property, root),
28677 length = values.length,
28678 hits = {},
28679 unique = [],
28680 value, strValue, i;
28681
28682 for (i = 0; i < length; i++) {
28683 value = values[i];
28684 strValue = String(value);
28685
28686 if ((allowNull || !Ext.isEmpty(value)) && !hits[strValue]) {
28687 hits[strValue] = true;
28688 unique.push(value);
28689 }
28690 }
28691
28692 return unique;
28693 },
28694
28695 /**
28696 * @private
28697 * Extracts all of the given property values from the items in the MixedCollection. Mainly used as a supporting method for
28698 * functions like `sum()` and `collect()`.
28699 * @param {String} property The property to extract.
28700 * @param {String} [root] Optional 'root' property to extract the first argument from. This is used mainly when
28701 * extracting field data from Model instances, where the fields are stored inside the `data` object.
28702 * @return {Array} The extracted values.
28703 */
28704 extractValues: function(property, root) {
28705 var values = this.items;
28706
28707 if (root) {
28708 values = Ext.Array.pluck(values, root);
28709 }
28710
28711 return Ext.Array.pluck(values, property);
28712 },
28713
28714 /**
28715 * Returns a range of items in this collection.
28716 * @param {Number} [start=0] The starting index.
28717 * @param {Number} [end=-1] The ending index.
28718 * @return {Array} An array of items
28719 */
28720 getRange: function(start, end){
28721 var me = this,
28722 items = me.items,
28723 range = [],
28724 i;
28725
28726 if (items.length < 1) {
28727 return range;
28728 }
28729
28730 start = start || 0;
28731 end = Math.min(typeof end == 'undefined' ? me.length - 1 : end, me.length - 1);
28732 if (start <= end) {
28733 for (i = start; i <= end; i++) {
28734 range[range.length] = items[i];
28735 }
28736 } else {
28737 for (i = start; i >= end; i--) {
28738 range[range.length] = items[i];
28739 }
28740 }
28741 return range;
28742 },
28743
28744 /**
28745 * Filters the objects in this collection by a set of {@link Ext.util.Filter Filter}s, or by a single
28746 * property/value pair with optional parameters for substring matching and case sensitivity. See
28747 * {@link Ext.util.Filter Filter} for an example of using Filter objects (preferred). Alternatively,
28748 * MixedCollection can be easily filtered by property like this:
28749 *
28750 * // create a simple store with a few people defined
28751 * var people = new Ext.util.MixedCollection();
28752 * people.addAll([
28753 * {id: 1, age: 25, name: 'Ed'},
28754 * {id: 2, age: 24, name: 'Tommy'},
28755 * {id: 3, age: 24, name: 'Arne'},
28756 * {id: 4, age: 26, name: 'Aaron'}
28757 * ]);
28758 *
28759 * // a new MixedCollection containing only the items where age == 24
28760 * var middleAged = people.filter('age', 24);
28761 *
28762 * @param {Ext.util.Filter[]/String} property A property on your objects, or an array of {@link Ext.util.Filter Filter} objects
28763 * @param {String/RegExp} value Either string that the property values
28764 * should start with or a RegExp to test against the property.
28765 * @param {Boolean} anyMatch (optional) `true` to match any part of the string, not just the beginning
28766 * @param {Boolean} [caseSensitive=false] (optional) `true` for case sensitive comparison.
28767 * @return {Ext.util.MixedCollection} The new filtered collection
28768 */
28769 filter: function(property, value, anyMatch, caseSensitive) {
28770 var filters = [],
28771 filterFn;
28772
28773 //support for the simple case of filtering by property/value
28774 if (Ext.isString(property)) {
28775 filters.push(Ext.create('Ext.util.Filter', {
28776 property : property,
28777 value : value,
28778 anyMatch : anyMatch,
28779 caseSensitive: caseSensitive
28780 }));
28781 } else if (Ext.isArray(property) || property instanceof Ext.util.Filter) {
28782 filters = filters.concat(property);
28783 }
28784
28785 //at this point we have an array of zero or more Ext.util.Filter objects to filter with,
28786 //so here we construct a function that combines these filters by ANDing them together
28787 filterFn = function(record) {
28788 var isMatch = true,
28789 length = filters.length,
28790 i;
28791
28792 for (i = 0; i < length; i++) {
28793 var filter = filters[i],
28794 fn = filter.getFilterFn(),
28795 scope = filter.getScope();
28796
28797 isMatch = isMatch && fn.call(scope, record);
28798 }
28799
28800 return isMatch;
28801 };
28802
28803 return this.filterBy(filterFn);
28804 },
28805
28806 /**
28807 * Filter by a function. Returns a _new_ collection that has been filtered.
28808 * The passed function will be called with each object in the collection.
28809 * If the function returns `true`, the value is included otherwise it is filtered.
28810 * @param {Function} fn The function to be called, it will receive the args `o` (the object), `k` (the key)
28811 * @param {Object} scope (optional) The scope (`this` reference) in which the function is executed. Defaults to this MixedCollection.
28812 * @return {Ext.util.MixedCollection} The new filtered collection.
28813 */
28814 filterBy: function(fn, scope) {
28815 var me = this,
28816 newMC = new this.self(),
28817 keys = me.keys,
28818 items = me.items,
28819 length = items.length,
28820 i;
28821
28822 newMC.getKey = me.getKey;
28823
28824 for (i = 0; i < length; i++) {
28825 if (fn.call(scope || me, items[i], keys[i])) {
28826 newMC.add(keys[i], items[i]);
28827 }
28828 }
28829
28830 return newMC;
28831 },
28832
28833 /**
28834 * Finds the index of the first matching object in this collection by a specific property/value.
28835 * @param {String} property The name of a property on your objects.
28836 * @param {String/RegExp} value A string that the property values.
28837 * should start with or a RegExp to test against the property.
28838 * @param {Number} [start=0] (optional) The index to start searching at.
28839 * @param {Boolean} anyMatch (optional) `true` to match any part of the string, not just the beginning.
28840 * @param {Boolean} caseSensitive (optional) `true` for case sensitive comparison.
28841 * @return {Number} The matched index or -1.
28842 */
28843 findIndex: function(property, value, start, anyMatch, caseSensitive){
28844 if(Ext.isEmpty(value, false)){
28845 return -1;
28846 }
28847 value = this.createValueMatcher(value, anyMatch, caseSensitive);
28848 return this.findIndexBy(function(o){
28849 return o && value.test(o[property]);
28850 }, null, start);
28851 },
28852
28853 /**
28854 * Find the index of the first matching object in this collection by a function.
28855 * If the function returns `true` it is considered a match.
28856 * @param {Function} fn The function to be called, it will receive the args `o` (the object), `k` (the key).
28857 * @param {Object} scope (optional) The scope (`this` reference) in which the function is executed. Defaults to this MixedCollection.
28858 * @param {Number} [start=0] (optional) The index to start searching at.
28859 * @return {Number} The matched index or -1.
28860 */
28861 findIndexBy: function(fn, scope, start){
28862 var me = this,
28863 keys = me.keys,
28864 items = me.items,
28865 i = start || 0,
28866 len = items.length;
28867
28868 for (; i < len; i++) {
28869 if (fn.call(scope || me, items[i], keys[i])) {
28870 return i;
28871 }
28872 }
28873 return -1;
28874 },
28875
28876 /**
28877 * Returns a regular expression based on the given value and matching options. This is used internally for finding and filtering,
28878 * and by Ext.data.Store#filter
28879 * @private
28880 * @param {String} value The value to create the regex for. This is escaped using Ext.escapeRe
28881 * @param {Boolean} [anyMatch=false] `true` to allow any match - no regex start/end line anchors will be added.
28882 * @param {Boolean} [caseSensitive=false] `true` to make the regex case sensitive (adds 'i' switch to regex).
28883 * @param {Boolean} [exactMatch=false] `true` to force exact match (^ and $ characters added to the regex). Ignored if `anyMatch` is `true`.
28884 */
28885 createValueMatcher: function(value, anyMatch, caseSensitive, exactMatch) {
28886 if (!value.exec) { // not a regex
28887 var er = Ext.String.escapeRegex;
28888 value = String(value);
28889
28890 if (anyMatch === true) {
28891 value = er(value);
28892 } else {
28893 value = '^' + er(value);
28894 if (exactMatch === true) {
28895 value += '$';
28896 }
28897 }
28898 value = new RegExp(value, caseSensitive ? '' : 'i');
28899 }
28900 return value;
28901 },
28902
28903 /**
28904 * Creates a shallow copy of this collection.
28905 * @return {Ext.util.MixedCollection}
28906 */
28907 clone: function() {
28908 var me = this,
28909 copy = new this.self(),
28910 keys = me.keys,
28911 items = me.items,
28912 i = 0,
28913 len = items.length;
28914
28915 for(; i < len; i++){
28916 copy.add(keys[i], items[i]);
28917 }
28918 copy.getKey = me.getKey;
28919 return copy;
28920 }
28921 });
28922
28923 /**
28924 * Represents a single sorter that can be used as part of the sorters configuration in Ext.mixin.Sortable.
28925 *
28926 * A common place for Sorters to be used are {@link Ext.data.Store Stores}. For example:
28927 *
28928 * @example miniphone
28929 * var store = Ext.create('Ext.data.Store', {
28930 * fields: ['firstName', 'lastName'],
28931 * sorters: 'lastName',
28932 *
28933 * data: [
28934 * { firstName: 'Tommy', lastName: 'Maintz' },
28935 * { firstName: 'Rob', lastName: 'Dougan' },
28936 * { firstName: 'Ed', lastName: 'Spencer'},
28937 * { firstName: 'Jamie', lastName: 'Avins' },
28938 * { firstName: 'Nick', lastName: 'Poulden'}
28939 * ]
28940 * });
28941 *
28942 * Ext.create('Ext.List', {
28943 * fullscreen: true,
28944 * itemTpl: '<div class="contact">{firstName} <strong>{lastName}</strong></div>',
28945 * store: store
28946 * });
28947 *
28948 * In the next example, we specify a custom sorter function:
28949 *
28950 * @example miniphone
28951 * var store = Ext.create('Ext.data.Store', {
28952 * fields: ['person'],
28953 * sorters: [
28954 * {
28955 * // Sort by first letter of last name, in descending order
28956 * sorterFn: function(record1, record2) {
28957 * var name1 = record1.data.person.name.split('-')[1].substr(0, 1),
28958 * name2 = record2.data.person.name.split('-')[1].substr(0, 1);
28959 *
28960 * return name1 > name2 ? 1 : (name1 === name2 ? 0 : -1);
28961 * },
28962 * direction: 'DESC'
28963 * }
28964 * ],
28965 * data: [
28966 * { person: { name: 'Tommy-Maintz' } },
28967 * { person: { name: 'Rob-Dougan' } },
28968 * { person: { name: 'Ed-Spencer' } },
28969 * { person: { name: 'Nick-Poulden' } },
28970 * { person: { name: 'Jamie-Avins' } }
28971 * ]
28972 * });
28973 *
28974 * Ext.create('Ext.List', {
28975 * fullscreen: true,
28976 * itemTpl: '{person.name}',
28977 * store: store
28978 * });
28979 */
28980 Ext.define('Ext.util.Sorter', {
28981 isSorter: true,
28982
28983 config: {
28984 /**
28985 * @cfg {String} property The property to sort by. Required unless `sorterFn` is provided
28986 */
28987 property: null,
28988
28989 /**
28990 * @cfg {Function} sorterFn A specific sorter function to execute. Can be passed instead of {@link #property}.
28991 * This function should compare the two passed arguments, returning -1, 0 or 1 depending on if item 1 should be
28992 * sorted before, at the same level, or after item 2.
28993 *
28994 * sorterFn: function(person1, person2) {
28995 * return (person1.age > person2.age) ? 1 : (person1.age === person2.age ? 0 : -1);
28996 * }
28997 */
28998 sorterFn: null,
28999
29000 /**
29001 * @cfg {String} root Optional root property. This is mostly useful when sorting a Store, in which case we set the
29002 * root to 'data' to make the filter pull the {@link #property} out of the data object of each item
29003 */
29004 root: null,
29005
29006 /**
29007 * @cfg {Function} transform A function that will be run on each value before
29008 * it is compared in the sorter. The function will receive a single argument,
29009 * the value.
29010 */
29011 transform: null,
29012
29013 /**
29014 * @cfg {String} direction The direction to sort by. Valid values are "ASC", and "DESC".
29015 */
29016 direction: "ASC",
29017
29018 /**
29019 * @cfg {Mixed} id An optional id this sorter can be keyed by in Collections. If
29020 * no id is specified it will use the property name used in this Sorter. If no
29021 * property is specified, e.g. when adding a custom sorter function we will generate
29022 * a random id.
29023 */
29024 id: undefined
29025 },
29026
29027 constructor: function(config) {
29028 this.initConfig(config);
29029 },
29030
29031 // <debug>
29032 applySorterFn: function(sorterFn) {
29033 if (!sorterFn && !this.getProperty()) {
29034 Ext.Logger.error("A Sorter requires either a property or a sorterFn.");
29035 }
29036 return sorterFn;
29037 },
29038
29039 applyProperty: function(property) {
29040 if (!property && !this.getSorterFn()) {
29041 Ext.Logger.error("A Sorter requires either a property or a sorterFn.");
29042 }
29043 return property;
29044 },
29045 // </debug>
29046
29047 applyId: function(id) {
29048 if (!id) {
29049 id = this.getProperty();
29050 if (!id) {
29051 id = Ext.id(null, 'ext-sorter-');
29052 }
29053 }
29054
29055 return id;
29056 },
29057
29058 /**
29059 * @private
29060 * Creates and returns a function which sorts an array by the given property and direction
29061 * @return {Function} A function which sorts by the property/direction combination provided
29062 */
29063 createSortFunction: function(sorterFn) {
29064 var me = this,
29065 modifier = me.getDirection().toUpperCase() == "DESC" ? -1 : 1;
29066
29067 //create a comparison function. Takes 2 objects, returns 1 if object 1 is greater,
29068 //-1 if object 2 is greater or 0 if they are equal
29069 return function(o1, o2) {
29070 return modifier * sorterFn.call(me, o1, o2);
29071 };
29072 },
29073
29074 /**
29075 * @private
29076 * Basic default sorter function that just compares the defined property of each object
29077 */
29078 defaultSortFn: function(item1, item2) {
29079 var me = this,
29080 transform = me._transform,
29081 root = me._root,
29082 value1, value2,
29083 property = me._property;
29084
29085 if (root !== null && root !== undefined) {
29086 item1 = item1[root];
29087 item2 = item2[root];
29088 }
29089
29090 value1 = item1[property];
29091 value2 = item2[property];
29092
29093 if (transform) {
29094 value1 = transform(value1);
29095 value2 = transform(value2);
29096 }
29097
29098 return value1 > value2 ? 1 : (value1 < value2 ? -1 : 0);
29099 },
29100
29101 updateDirection: function() {
29102 this.updateSortFn();
29103 },
29104
29105 updateSortFn: function() {
29106 this.sort = this.createSortFunction(this.getSorterFn() || this.defaultSortFn);
29107 },
29108
29109 /**
29110 * Toggles the direction of this Sorter. Note that when you call this function,
29111 * the Collection this Sorter is part of does not get refreshed automatically.
29112 */
29113 toggle: function() {
29114 this.setDirection(Ext.String.toggle(this.getDirection(), "ASC", "DESC"));
29115 }
29116 });
29117
29118 /**
29119 * @docauthor Tommy Maintz <tommy@sencha.com>
29120 *
29121 * 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}.
29122 *
29123 * __Note:__ This mixin is mainly for internal library use and most users should not need to use it directly. It
29124 * is more likely you will want to use one of the component classes that import this mixin, such as
29125 * {@link Ext.data.Store} or {@link Ext.data.TreeStore}.
29126 */
29127 Ext.define("Ext.util.Sortable", {
29128 extend: Ext.mixin.Mixin ,
29129 /**
29130 * @property {Boolean} isSortable
29131 * Flag denoting that this object is sortable. Always `true`.
29132 * @readonly
29133 */
29134 isSortable: true,
29135
29136 mixinConfig: {
29137 hooks: {
29138 destroy: 'destroy'
29139 }
29140 },
29141
29142 /**
29143 * @property {String} defaultSortDirection
29144 * The default sort direction to use if one is not specified.
29145 */
29146 defaultSortDirection: "ASC",
29147
29148
29149
29150
29151
29152 /**
29153 * @property {String} sortRoot
29154 * The property in each item that contains the data to sort.
29155 */
29156
29157 /**
29158 * Performs initialization of this mixin. Component classes using this mixin should call this method during their
29159 * own initialization.
29160 */
29161 initSortable: function() {
29162 var me = this,
29163 sorters = me.sorters;
29164
29165 /**
29166 * @property {Ext.util.MixedCollection} sorters
29167 * The collection of {@link Ext.util.Sorter Sorters} currently applied to this Store
29168 */
29169 me.sorters = Ext.create('Ext.util.AbstractMixedCollection', false, function(item) {
29170 return item.id || item.property;
29171 });
29172
29173 if (sorters) {
29174 me.sorters.addAll(me.decodeSorters(sorters));
29175 }
29176 },
29177
29178 /**
29179 * Sorts the data in the Store by one or more of its properties. Example usage:
29180 *
29181 * //sort by a single field
29182 * myStore.sort('myField', 'DESC');
29183 *
29184 * //sorting by multiple fields
29185 * myStore.sort([
29186 * {
29187 * property : 'age',
29188 * direction: 'ASC'
29189 * },
29190 * {
29191 * property : 'name',
29192 * direction: 'DESC'
29193 * }
29194 * ]);
29195 *
29196 * Internally, Store converts the passed arguments into an array of {@link Ext.util.Sorter} instances, and delegates
29197 * the actual sorting to its internal {@link Ext.util.MixedCollection}.
29198 *
29199 * When passing a single string argument to sort, Store maintains a ASC/DESC toggler per field, so this code:
29200 *
29201 * store.sort('myField');
29202 * store.sort('myField');
29203 *
29204 * Is equivalent to this code, because Store handles the toggling automatically:
29205 *
29206 * store.sort('myField', 'ASC');
29207 * store.sort('myField', 'DESC');
29208 *
29209 * @param {String/Ext.util.Sorter[]} sorters Either a string name of one of the fields in this Store's configured
29210 * {@link Ext.data.Model Model}, or an array of sorter configurations.
29211 * @param {String} [direction="ASC"] The overall direction to sort the data by.
29212 * @param {String} [where]
29213 * @param {Boolean} [doSort]
29214 * @return {Ext.util.Sorter[]}
29215 */
29216 sort: function(sorters, direction, where, doSort) {
29217 var me = this,
29218 sorter, sorterFn,
29219 newSorters;
29220
29221 if (Ext.isArray(sorters)) {
29222 doSort = where;
29223 where = direction;
29224 newSorters = sorters;
29225 }
29226 else if (Ext.isObject(sorters)) {
29227 doSort = where;
29228 where = direction;
29229 newSorters = [sorters];
29230 }
29231 else if (Ext.isString(sorters)) {
29232 sorter = me.sorters.get(sorters);
29233
29234 if (!sorter) {
29235 sorter = {
29236 property : sorters,
29237 direction: direction
29238 };
29239 newSorters = [sorter];
29240 }
29241 else if (direction === undefined) {
29242 sorter.toggle();
29243 }
29244 else {
29245 sorter.setDirection(direction);
29246 }
29247 }
29248
29249 if (newSorters && newSorters.length) {
29250 newSorters = me.decodeSorters(newSorters);
29251 if (Ext.isString(where)) {
29252 if (where === 'prepend') {
29253 sorters = me.sorters.clone().items;
29254
29255 me.sorters.clear();
29256 me.sorters.addAll(newSorters);
29257 me.sorters.addAll(sorters);
29258 }
29259 else {
29260 me.sorters.addAll(newSorters);
29261 }
29262 }
29263 else {
29264 me.sorters.clear();
29265 me.sorters.addAll(newSorters);
29266 }
29267
29268 if (doSort !== false) {
29269 me.onBeforeSort(newSorters);
29270 }
29271 }
29272
29273 if (doSort !== false) {
29274 sorters = me.sorters.items;
29275 if (sorters.length) {
29276 //construct an amalgamated sorter function which combines all of the Sorters passed
29277 sorterFn = function(r1, r2) {
29278 var result = sorters[0].sort(r1, r2),
29279 length = sorters.length,
29280 i;
29281
29282 //if we have more than one sorter, OR any additional sorter functions together
29283 for (i = 1; i < length; i++) {
29284 result = result || sorters[i].sort.call(this, r1, r2);
29285 }
29286
29287 return result;
29288 };
29289
29290 me.doSort(sorterFn);
29291 }
29292 }
29293
29294 return sorters;
29295 },
29296
29297 onBeforeSort: Ext.emptyFn,
29298
29299 /**
29300 * @private
29301 * Normalizes an array of sorter objects, ensuring that they are all {@link Ext.util.Sorter} instances.
29302 * @param {Array} sorters The sorters array.
29303 * @return {Array} Array of {@link Ext.util.Sorter} objects.
29304 */
29305 decodeSorters: function(sorters) {
29306 if (!Ext.isArray(sorters)) {
29307 if (sorters === undefined) {
29308 sorters = [];
29309 } else {
29310 sorters = [sorters];
29311 }
29312 }
29313
29314 var length = sorters.length,
29315 Sorter = Ext.util.Sorter,
29316 fields = this.model ? this.model.prototype.fields : null,
29317 field,
29318 config, i;
29319
29320 for (i = 0; i < length; i++) {
29321 config = sorters[i];
29322
29323 if (!(config instanceof Sorter)) {
29324 if (Ext.isString(config)) {
29325 config = {
29326 property: config
29327 };
29328 }
29329
29330 Ext.applyIf(config, {
29331 root : this.sortRoot,
29332 direction: "ASC"
29333 });
29334
29335 if (config.fn) {
29336 config.sorterFn = config.fn;
29337 }
29338
29339 //support a function to be passed as a sorter definition
29340 if (typeof config == 'function') {
29341 config = {
29342 sorterFn: config
29343 };
29344 }
29345
29346 // ensure sortType gets pushed on if necessary
29347 if (fields && !config.transform) {
29348 field = fields.get(config.property);
29349 config.transform = field ? field.sortType : undefined;
29350 }
29351 sorters[i] = Ext.create('Ext.util.Sorter', config);
29352 }
29353 }
29354
29355 return sorters;
29356 },
29357
29358 getSorters: function() {
29359 return this.sorters.items;
29360 },
29361
29362 destroy: function () {
29363 this.callSuper();
29364 Ext.destroy(this.sorters);
29365 }
29366 });
29367
29368 /**
29369 * Represents a collection of a set of key and value pairs. Each key in the MixedCollection must be unique, the same key
29370 * cannot exist twice. This collection is ordered, items in the collection can be accessed by index or via the key.
29371 * Newly added items are added to the end of the collection. This class is similar to {@link Ext.util.HashMap} however
29372 * it is heavier and provides more functionality. Sample usage:
29373 *
29374 * @example
29375 * var coll = new Ext.util.MixedCollection();
29376 * coll.add('key1', 'val1');
29377 * coll.add('key2', 'val2');
29378 * coll.add('key3', 'val3');
29379 *
29380 * alert(coll.get('key1')); // 'val1'
29381 * alert(coll.indexOfKey('key3')); // 2
29382 *
29383 * The MixedCollection also has support for sorting and filtering of the values in the collection.
29384 *
29385 * @example
29386 * var coll = new Ext.util.MixedCollection();
29387 * coll.add('key1', 100);
29388 * coll.add('key2', -100);
29389 * coll.add('key3', 17);
29390 * coll.add('key4', 0);
29391 * var biggerThanZero = coll.filterBy(function(value){
29392 * return value > 0;
29393 * });
29394 * alert(biggerThanZero.getCount()); // 2
29395 */
29396 Ext.define('Ext.util.MixedCollection', {
29397 extend: Ext.util.AbstractMixedCollection ,
29398 mixins: {
29399 sortable: Ext.util.Sortable
29400 },
29401
29402 /**
29403 * @event sort
29404 * Fires whenever MixedCollection is sorted.
29405 * @param {Ext.util.MixedCollection} this
29406 */
29407
29408 constructor: function() {
29409 var me = this;
29410 me.callParent(arguments);
29411 me.mixins.sortable.initSortable.call(me);
29412 },
29413
29414 doSort: function(sorterFn) {
29415 this.sortBy(sorterFn);
29416 },
29417
29418 /**
29419 * @private
29420 * Performs the actual sorting based on a direction and a sorting function. Internally,
29421 * this creates a temporary array of all items in the MixedCollection, sorts it and then writes
29422 * the sorted array data back into `this.items` and `this.keys`.
29423 * @param {String} property Property to sort by ('key', 'value', or 'index')
29424 * @param {String} [dir=ASC] (optional) Direction to sort 'ASC' or 'DESC'.
29425 * @param {Function} fn (optional) Comparison function that defines the sort order.
29426 * Defaults to sorting by numeric value.
29427 */
29428 _sort: function(property, dir, fn){
29429 var me = this,
29430 i, len,
29431 dsc = String(dir).toUpperCase() == 'DESC' ? -1 : 1,
29432
29433 //this is a temporary array used to apply the sorting function
29434 c = [],
29435 keys = me.keys,
29436 items = me.items;
29437
29438 //default to a simple sorter function if one is not provided
29439 fn = fn || function(a, b) {
29440 return a - b;
29441 };
29442
29443 //copy all the items into a temporary array, which we will sort
29444 for(i = 0, len = items.length; i < len; i++){
29445 c[c.length] = {
29446 key : keys[i],
29447 value: items[i],
29448 index: i
29449 };
29450 }
29451
29452 //sort the temporary array
29453 Ext.Array.sort(c, function(a, b){
29454 var v = fn(a[property], b[property]) * dsc;
29455 if(v === 0){
29456 v = (a.index < b.index ? -1 : 1);
29457 }
29458 return v;
29459 });
29460
29461 //copy the temporary array back into the main this.items and this.keys objects
29462 for(i = 0, len = c.length; i < len; i++){
29463 items[i] = c[i].value;
29464 keys[i] = c[i].key;
29465 }
29466
29467 me.fireEvent('sort', me);
29468 },
29469
29470 /**
29471 * Sorts the collection by a single sorter function.
29472 * @param {Function} sorterFn The function to sort by.
29473 */
29474 sortBy: function(sorterFn) {
29475 var me = this,
29476 items = me.items,
29477 keys = me.keys,
29478 length = items.length,
29479 temp = [],
29480 i;
29481
29482 //first we create a copy of the items array so that we can sort it
29483 for (i = 0; i < length; i++) {
29484 temp[i] = {
29485 key : keys[i],
29486 value: items[i],
29487 index: i
29488 };
29489 }
29490
29491 Ext.Array.sort(temp, function(a, b) {
29492 var v = sorterFn(a.value, b.value);
29493 if (v === 0) {
29494 v = (a.index < b.index ? -1 : 1);
29495 }
29496
29497 return v;
29498 });
29499
29500 //copy the temporary array back into the main this.items and this.keys objects
29501 for (i = 0; i < length; i++) {
29502 items[i] = temp[i].value;
29503 keys[i] = temp[i].key;
29504 }
29505
29506 me.fireEvent('sort', me, items, keys);
29507 },
29508
29509 /**
29510 * Reorders each of the items based on a mapping from old index to new index. Internally this just translates into a
29511 * sort. The `sort` event is fired whenever reordering has occured.
29512 * @param {Object} mapping Mapping from old item index to new item index.
29513 */
29514 reorder: function(mapping) {
29515 var me = this,
29516 items = me.items,
29517 index = 0,
29518 length = items.length,
29519 order = [],
29520 remaining = [],
29521 oldIndex;
29522
29523 me.suspendEvents();
29524
29525 //object of {oldPosition: newPosition} reversed to {newPosition: oldPosition}
29526 for (oldIndex in mapping) {
29527 order[mapping[oldIndex]] = items[oldIndex];
29528 }
29529
29530 for (index = 0; index < length; index++) {
29531 if (mapping[index] == undefined) {
29532 remaining.push(items[index]);
29533 }
29534 }
29535
29536 for (index = 0; index < length; index++) {
29537 if (order[index] == undefined) {
29538 order[index] = remaining.shift();
29539 }
29540 }
29541
29542 me.clear();
29543 me.addAll(order);
29544
29545 me.resumeEvents();
29546 me.fireEvent('sort', me);
29547 },
29548
29549 /**
29550 * Sorts this collection by **key**s.
29551 * @param {String} [dir=ASC] Sorting direction: 'ASC' or 'DESC'.
29552 * @param {Function} [fn] Comparison function that defines the sort order. Defaults to sorting by case insensitive
29553 * string.
29554 */
29555 sortByKey: function(dir, fn){
29556 this._sort('key', dir, fn || function(a, b){
29557 var v1 = String(a).toUpperCase(), v2 = String(b).toUpperCase();
29558 return v1 > v2 ? 1 : (v1 < v2 ? -1 : 0);
29559 });
29560 }
29561 });
29562
29563 /**
29564 * @private
29565 */
29566 Ext.define('Ext.ItemCollection', {
29567 extend: Ext.util.MixedCollection ,
29568
29569 getKey: function(item) {
29570 return item.getItemId();
29571 },
29572
29573 has: function(item) {
29574 return this.map.hasOwnProperty(item.getId());
29575 }
29576 });
29577
29578 /**
29579 * @private
29580 */
29581 Ext.define('Ext.fx.easing.Momentum', {
29582
29583 extend: Ext.fx.easing.Abstract ,
29584
29585 config: {
29586 acceleration: 30,
29587 friction: 0,
29588 startVelocity: 0
29589 },
29590
29591 alpha: 0,
29592
29593 updateFriction: function(friction) {
29594 var theta = Math.log(1 - (friction / 10));
29595
29596 this.theta = theta;
29597
29598 this.alpha = theta / this.getAcceleration();
29599 },
29600
29601 updateStartVelocity: function(velocity) {
29602 this.velocity = velocity * this.getAcceleration();
29603 },
29604
29605 updateAcceleration: function(acceleration) {
29606 this.velocity = this.getStartVelocity() * acceleration;
29607
29608 this.alpha = this.theta / acceleration;
29609 },
29610
29611 getValue: function() {
29612 return this.getStartValue() - this.velocity * (1 - this.getFrictionFactor()) / this.theta;
29613 },
29614
29615 getFrictionFactor: function() {
29616 var deltaTime = Ext.Date.now() - this.getStartTime();
29617
29618 return Math.exp(deltaTime * this.alpha);
29619 },
29620
29621 getVelocity: function() {
29622 return this.getFrictionFactor() * this.velocity;
29623 }
29624 });
29625
29626 /**
29627 * @private
29628 */
29629 Ext.define('Ext.fx.easing.Bounce', {
29630
29631 extend: Ext.fx.easing.Abstract ,
29632
29633 config: {
29634 springTension: 0.3,
29635 acceleration: 30,
29636 startVelocity: 0
29637 },
29638
29639 getValue: function() {
29640 var deltaTime = Ext.Date.now() - this.getStartTime(),
29641 theta = (deltaTime / this.getAcceleration()),
29642 powTime = theta * Math.pow(Math.E, -this.getSpringTension() * theta);
29643
29644 return this.getStartValue() + (this.getStartVelocity() * powTime);
29645 }
29646 });
29647
29648 /**
29649 * @private
29650 *
29651 * This easing is typically used for {@link Ext.scroll.Scroller}. It's a combination of
29652 * {@link Ext.fx.easing.Momentum} and {@link Ext.fx.easing.Bounce}, which emulates deceleration when the animated element
29653 * is still within its boundary, then bouncing back (snapping) when it's out-of-bound.
29654 */
29655
29656 Ext.define('Ext.fx.easing.BoundMomentum', {
29657 extend: Ext.fx.easing.Abstract ,
29658
29659
29660
29661
29662
29663
29664 config: {
29665 /**
29666 * @cfg {Object} momentum
29667 * A valid config object for {@link Ext.fx.easing.Momentum}
29668 * @accessor
29669 */
29670 momentum: null,
29671
29672 /**
29673 * @cfg {Object} bounce
29674 * A valid config object for {@link Ext.fx.easing.Bounce}
29675 * @accessor
29676 */
29677 bounce: null,
29678
29679 minMomentumValue: 0,
29680
29681 maxMomentumValue: 0,
29682
29683 /**
29684 * @cfg {Number} minVelocity
29685 * The minimum velocity to end this easing
29686 * @accessor
29687 */
29688 minVelocity: 0.01,
29689
29690 /**
29691 * @cfg {Number} startVelocity
29692 * The start velocity
29693 * @accessor
29694 */
29695 startVelocity: 0
29696 },
29697
29698 applyMomentum: function(config, currentEasing) {
29699 return Ext.factory(config, Ext.fx.easing.Momentum, currentEasing);
29700 },
29701
29702 applyBounce: function(config, currentEasing) {
29703 return Ext.factory(config, Ext.fx.easing.Bounce, currentEasing);
29704 },
29705
29706 updateStartTime: function(startTime) {
29707 this.getMomentum().setStartTime(startTime);
29708
29709 this.callParent(arguments);
29710 },
29711
29712 updateStartVelocity: function(startVelocity) {
29713 this.getMomentum().setStartVelocity(startVelocity);
29714 },
29715
29716 updateStartValue: function(startValue) {
29717 this.getMomentum().setStartValue(startValue);
29718 },
29719
29720 reset: function() {
29721 this.lastValue = null;
29722
29723 this.isBouncingBack = false;
29724
29725 this.isOutOfBound = false;
29726
29727 return this.callParent(arguments);
29728 },
29729
29730 getValue: function() {
29731 var momentum = this.getMomentum(),
29732 bounce = this.getBounce(),
29733 startVelocity = momentum.getStartVelocity(),
29734 direction = startVelocity > 0 ? 1 : -1,
29735 minValue = this.getMinMomentumValue(),
29736 maxValue = this.getMaxMomentumValue(),
29737 boundedValue = (direction == 1) ? maxValue : minValue,
29738 lastValue = this.lastValue,
29739 value, velocity;
29740
29741 if (startVelocity === 0) {
29742 return this.getStartValue();
29743 }
29744
29745 if (!this.isOutOfBound) {
29746 value = momentum.getValue();
29747 velocity = momentum.getVelocity();
29748
29749 if (Math.abs(velocity) < this.getMinVelocity()) {
29750 this.isEnded = true;
29751 }
29752
29753 if (value >= minValue && value <= maxValue) {
29754 return value;
29755 }
29756
29757 this.isOutOfBound = true;
29758
29759 bounce.setStartTime(Ext.Date.now())
29760 .setStartVelocity(velocity)
29761 .setStartValue(boundedValue);
29762 }
29763
29764 value = bounce.getValue();
29765
29766 if (!this.isEnded) {
29767 if (!this.isBouncingBack) {
29768 if (lastValue !== null) {
29769 if ((direction == 1 && value < lastValue) || (direction == -1 && value > lastValue)) {
29770 this.isBouncingBack = true;
29771 }
29772 }
29773 }
29774 else {
29775 if (Math.round(value) == boundedValue) {
29776 this.isEnded = true;
29777 }
29778 }
29779 }
29780
29781 this.lastValue = value;
29782
29783 return value;
29784 }
29785 });
29786
29787 /**
29788 * @private
29789 */
29790 Ext.define('Ext.fx.easing.EaseOut', {
29791 extend: Ext.fx.easing.Linear ,
29792
29793 alias: 'easing.ease-out',
29794
29795 config: {
29796 exponent: 4,
29797 duration: 1500
29798 },
29799
29800 getValue: function() {
29801 var deltaTime = Ext.Date.now() - this.getStartTime(),
29802 duration = this.getDuration(),
29803 startValue = this.getStartValue(),
29804 endValue = this.getEndValue(),
29805 distance = this.distance,
29806 theta = deltaTime / duration,
29807 thetaC = 1 - theta,
29808 thetaEnd = 1 - Math.pow(thetaC, this.getExponent()),
29809 currentValue = startValue + (thetaEnd * distance);
29810
29811 if (deltaTime >= duration) {
29812 this.isEnded = true;
29813 return endValue;
29814 }
29815
29816 return currentValue;
29817 }
29818 });
29819
29820 /**
29821 * @class Ext.scroll.Scroller
29822 * @author Jacky Nguyen <jacky@sencha.com>
29823 *
29824 * Momentum scrolling is one of the most important part of the framework's UI layer. In Sencha Touch there are
29825 * several scroller implementations so we can have the best performance on all mobile devices and browsers.
29826 *
29827 * Scroller settings can be changed using the {@link Ext.Container#scrollable scrollable} configuration in
29828 * {@link Ext.Container}. Anything you pass to that method will be passed to the scroller when it is
29829 * instantiated in your container.
29830 *
29831 * Please note that the {@link Ext.Container#getScrollable} method returns an instance of {@link Ext.scroll.View}.
29832 * So if you need to get access to the scroller after your container has been instantiated, you must use the
29833 * {@link Ext.scroll.View#getScroller} method.
29834 *
29835 * // lets assume container is a container you have
29836 * // created which is scrollable
29837 * container.getScrollable().getScroller().setFps(10);
29838 *
29839 * ## Example
29840 *
29841 * Here is a simple example of how to adjust the scroller settings when using a {@link Ext.Container} (or anything
29842 * that extends it).
29843 *
29844 * @example
29845 * Ext.create('Ext.Container', {
29846 * fullscreen: true,
29847 * html: "Macaroni cheese roquefort<br>" +
29848 * "port-salut. The big cheese<br>" +
29849 * "fondue camembert de normandie<br>" +
29850 * "cow boursin cheese swiss stinking<br>" +
29851 * "bishop. Fromage feta edam fromage<br>" +
29852 * "frais bavarian bergkase paneer<br>" +
29853 * "paneer cheese and wine. Cow danish<br>" +
29854 * "fontina roquefort bocconcini<br>" +
29855 * "jarlsberg parmesan cheesecake<br>" +
29856 * "danish fontina. Mascarpone<br>" +
29857 * "bishop. Fromage feta edam fromage<br>" +
29858 * "frais bavarian bergkase paneer<br>" +
29859 * "paneer cheese and wine. Cow danish<br>" +
29860 * "fontina roquefort bocconcini<br>" +
29861 * "jarlsberg parmesan cheesecake<br>" +
29862 * "emmental fromage frais cheesy<br>" +
29863 * "grin say cheese squirty cheese<br>" +
29864 * "parmesan queso. Cheese triangles<br>" +
29865 * "st. agur blue cheese chalk and cheese<br>" +
29866 * "cream cheese lancashire manchego<br>" +
29867 * "taleggio blue castello. Port-salut<br>" +
29868 * "paneer monterey jack<br>" +
29869 * "say cheese fondue.",
29870 * scrollable: {
29871 * direction: 'vertical'
29872 * }
29873 * });
29874 *
29875 * As you can see, we are passing the {@link #direction} configuration into the scroller instance in our container.
29876 *
29877 * You can pass any of the configs below in that {@link Ext.Container#scrollable scrollable} configuration and it will
29878 * just work.
29879 *
29880 * Go ahead and try it in the live code editor above!
29881 */
29882 Ext.define('Ext.scroll.Scroller', {
29883
29884 extend: Ext.Evented ,
29885
29886
29887
29888
29889
29890
29891
29892 /**
29893 * @event maxpositionchange
29894 * Fires whenever the maximum position has changed.
29895 * @param {Ext.scroll.Scroller} this
29896 * @param {Number} maxPosition The new maximum position.
29897 */
29898
29899 /**
29900 * @event refresh
29901 * Fires whenever the Scroller is refreshed.
29902 * @param {Ext.scroll.Scroller} this
29903 */
29904
29905 /**
29906 * @event scrollstart
29907 * Fires whenever the scrolling is started.
29908 * @param {Ext.scroll.Scroller} this
29909 * @param {Number} x The current x position.
29910 * @param {Number} y The current y position.
29911 */
29912
29913 /**
29914 * @event scrollend
29915 * Fires whenever the scrolling is ended.
29916 * @param {Ext.scroll.Scroller} this
29917 * @param {Number} x The current x position.
29918 * @param {Number} y The current y position.
29919 */
29920
29921 /**
29922 * @event scroll
29923 * Fires whenever the Scroller is scrolled.
29924 * @param {Ext.scroll.Scroller} this
29925 * @param {Number} x The new x position.
29926 * @param {Number} y The new y position.
29927 */
29928
29929 config: {
29930 /**
29931 * @cfg element
29932 * @private
29933 */
29934 element: null,
29935
29936 /**
29937 * @cfg {String} direction
29938 * Possible values: 'auto', 'vertical', 'horizontal', or 'both'.
29939 * @accessor
29940 */
29941 direction: 'auto',
29942
29943 /**
29944 * @cfg fps
29945 * @private
29946 */
29947 fps: 'auto',
29948
29949 /**
29950 * @cfg {Boolean} disabled
29951 * Whether or not this component is disabled.
29952 * @accessor
29953 */
29954 disabled: null,
29955
29956 /**
29957 * @cfg {Boolean} directionLock
29958 * `true` to lock the direction of the scroller when the user starts scrolling.
29959 * This is useful when putting a scroller inside a scroller or a {@link Ext.Carousel}.
29960 * @accessor
29961 */
29962 directionLock: false,
29963
29964 /**
29965 * @cfg {Object} momentumEasing
29966 * A valid config for {@link Ext.fx.easing.BoundMomentum}. The default value is:
29967 *
29968 * {
29969 * momentum: {
29970 * acceleration: 30,
29971 * friction: 0.5
29972 * },
29973 * bounce: {
29974 * acceleration: 30,
29975 * springTension: 0.3
29976 * }
29977 * }
29978 *
29979 * Note that supplied object will be recursively merged with the default object. For example, you can simply
29980 * pass this to change the momentum acceleration only:
29981 *
29982 * {
29983 * momentum: {
29984 * acceleration: 10
29985 * }
29986 * }
29987 *
29988 * @accessor
29989 */
29990 momentumEasing: {
29991 momentum: {
29992 acceleration: 30,
29993 friction: 0.5
29994 },
29995
29996 bounce: {
29997 acceleration: 30,
29998 springTension: 0.3
29999 },
30000
30001 minVelocity: 1
30002 },
30003
30004 /**
30005 * @cfg bounceEasing
30006 * @private
30007 */
30008 bounceEasing: {
30009 duration: 400
30010 },
30011
30012 /**
30013 * @cfg outOfBoundRestrictFactor
30014 * @private
30015 */
30016 outOfBoundRestrictFactor: 0.5,
30017
30018 /**
30019 * @cfg startMomentumResetTime
30020 * @private
30021 */
30022 startMomentumResetTime: 300,
30023
30024 /**
30025 * @cfg maxAbsoluteVelocity
30026 * @private
30027 */
30028 maxAbsoluteVelocity: 6,
30029
30030 /**
30031 * @cfg containerSize
30032 * @private
30033 */
30034 containerSize: 'auto',
30035
30036 /**
30037 * @cfg size
30038 * @private
30039 */
30040 size: 'auto',
30041
30042 /**
30043 * @cfg autoRefresh
30044 * @private
30045 */
30046 autoRefresh: true,
30047
30048 /**
30049 * @cfg {Object/Number} initialOffset
30050 * The initial scroller position. When specified as Number,
30051 * both x and y will be set to that value.
30052 */
30053 initialOffset: {
30054 x: 0,
30055 y: 0
30056 },
30057
30058 /**
30059 * @cfg {Number/Object} slotSnapSize
30060 * The size of each slot to snap to in 'px', can be either an object with `x` and `y` values, i.e:
30061 *
30062 * {
30063 * x: 50,
30064 * y: 100
30065 * }
30066 *
30067 * or a number value to be used for both directions. For example, a value of `50` will be treated as:
30068 *
30069 * {
30070 * x: 50,
30071 * y: 50
30072 * }
30073 *
30074 * @accessor
30075 */
30076 slotSnapSize: {
30077 x: 0,
30078 y: 0
30079 },
30080
30081 /**
30082 * @cfg slotSnapOffset
30083 * @private
30084 */
30085 slotSnapOffset: {
30086 x: 0,
30087 y: 0
30088 },
30089
30090 slotSnapEasing: {
30091 duration: 150
30092 },
30093
30094 translatable: {
30095 translationMethod: 'auto',
30096 useWrapper: false
30097 }
30098 },
30099
30100 cls: Ext.baseCSSPrefix + 'scroll-scroller',
30101
30102 containerCls: Ext.baseCSSPrefix + 'scroll-container',
30103
30104 dragStartTime: 0,
30105
30106 dragEndTime: 0,
30107
30108 isDragging: false,
30109
30110 isAnimating: false,
30111
30112 /**
30113 * @private
30114 * @constructor
30115 * @chainable
30116 */
30117 constructor: function(config) {
30118 var element = config && config.element;
30119
30120 this.listeners = {
30121 scope: this,
30122 touchstart: 'onTouchStart',
30123 touchend: 'onTouchEnd',
30124 dragstart: 'onDragStart',
30125 drag: 'onDrag',
30126 dragend: 'onDragEnd'
30127 };
30128
30129 this.minPosition = { x: 0, y: 0 };
30130
30131 this.startPosition = { x: 0, y: 0 };
30132
30133 this.position = { x: 0, y: 0 };
30134
30135 this.velocity = { x: 0, y: 0 };
30136
30137 this.isAxisEnabledFlags = { x: false, y: false };
30138
30139 this.flickStartPosition = { x: 0, y: 0 };
30140
30141 this.flickStartTime = { x: 0, y: 0 };
30142
30143 this.lastDragPosition = { x: 0, y: 0 };
30144
30145 this.dragDirection = { x: 0, y: 0};
30146
30147 this.initialConfig = config;
30148
30149 if (element) {
30150 this.setElement(element);
30151 }
30152
30153 return this;
30154 },
30155
30156 /**
30157 * @private
30158 */
30159 applyElement: function(element) {
30160 if (!element) {
30161 return;
30162 }
30163
30164 return Ext.get(element);
30165 },
30166
30167 /**
30168 * @private
30169 * @chainable
30170 */
30171 updateElement: function(element) {
30172 this.initialize();
30173
30174 if (!this.FixedHBoxStretching) {
30175 element.addCls(this.cls);
30176 }
30177
30178 if (!this.getDisabled()) {
30179 this.attachListeneners();
30180 }
30181
30182 this.onConfigUpdate(['containerSize', 'size'], 'refreshMaxPosition');
30183
30184 this.on('maxpositionchange', 'snapToBoundary');
30185 this.on('minpositionchange', 'snapToBoundary');
30186
30187 return this;
30188 },
30189
30190 applyTranslatable: function(config, translatable) {
30191 return Ext.factory(config, Ext.util.Translatable, translatable);
30192 },
30193
30194 updateTranslatable: function(translatable) {
30195 translatable.setConfig({
30196 element: this.getElement(),
30197 listeners: {
30198 animationframe: 'onAnimationFrame',
30199 animationend: 'onAnimationEnd',
30200 scope: this
30201 }
30202 });
30203 },
30204
30205 updateFps: function(fps) {
30206 if (fps !== 'auto') {
30207 this.getTranslatable().setFps(fps);
30208 }
30209 },
30210
30211 /**
30212 * @private
30213 */
30214 attachListeneners: function() {
30215 this.getContainer().on(this.listeners);
30216 },
30217
30218 /**
30219 * @private
30220 */
30221 detachListeners: function() {
30222 this.getContainer().un(this.listeners);
30223 },
30224
30225 /**
30226 * @private
30227 */
30228 updateDisabled: function(disabled) {
30229 if (disabled) {
30230 this.detachListeners();
30231 }
30232 else {
30233 this.attachListeneners();
30234 }
30235 },
30236
30237 updateInitialOffset: function(initialOffset) {
30238 if (typeof initialOffset == 'number') {
30239 initialOffset = {
30240 x: initialOffset,
30241 y: initialOffset
30242 };
30243 }
30244
30245 var position = this.position,
30246 x, y;
30247
30248 position.x = x = initialOffset.x;
30249 position.y = y = initialOffset.y;
30250
30251 this.getTranslatable().translate(-x, -y);
30252 },
30253
30254 /**
30255 * @private
30256 * @return {String}
30257 */
30258 applyDirection: function(direction) {
30259 var minPosition = this.getMinPosition(),
30260 maxPosition = this.getMaxPosition(),
30261 isHorizontal, isVertical;
30262
30263 this.givenDirection = direction;
30264
30265 if (direction === 'auto') {
30266 isHorizontal = maxPosition.x > minPosition.x;
30267 isVertical = maxPosition.y > minPosition.y;
30268
30269 if (isHorizontal && isVertical) {
30270 direction = 'both';
30271 }
30272 else if (isHorizontal) {
30273 direction = 'horizontal';
30274 }
30275 else {
30276 direction = 'vertical';
30277 }
30278 }
30279
30280 return direction;
30281 },
30282
30283 /**
30284 * @private
30285 */
30286 updateDirection: function(direction, oldDirection) {
30287 var isAxisEnabledFlags = this.isAxisEnabledFlags,
30288 verticalCls = this.cls + '-vertical',
30289 horizontalCls = this.cls + '-horizontal',
30290 element = this.getElement();
30291
30292 if (oldDirection === 'both' || oldDirection === 'horizontal') {
30293 element.removeCls(horizontalCls);
30294 }
30295
30296 if (oldDirection === 'both' || oldDirection === 'vertical') {
30297 element.removeCls(verticalCls);
30298 }
30299
30300 isAxisEnabledFlags.x = isAxisEnabledFlags.y = false;
30301 if (direction === 'both' || direction === 'horizontal') {
30302 isAxisEnabledFlags.x = true;
30303 element.addCls(horizontalCls);
30304 }
30305
30306 if (direction === 'both' || direction === 'vertical') {
30307 isAxisEnabledFlags.y = true;
30308 element.addCls(verticalCls);
30309 }
30310 },
30311
30312 /**
30313 * Returns `true` if a specified axis is enabled.
30314 * @param {String} axis The axis to check (`x` or `y`).
30315 * @return {Boolean} `true` if the axis is enabled.
30316 */
30317 isAxisEnabled: function(axis) {
30318 this.getDirection();
30319
30320 return this.isAxisEnabledFlags[axis];
30321 },
30322
30323 /**
30324 * @private
30325 * @return {Object}
30326 */
30327 applyMomentumEasing: function(easing) {
30328 var defaultClass = Ext.fx.easing.BoundMomentum;
30329
30330 return {
30331 x: Ext.factory(easing, defaultClass),
30332 y: Ext.factory(easing, defaultClass)
30333 };
30334 },
30335
30336 /**
30337 * @private
30338 * @return {Object}
30339 */
30340 applyBounceEasing: function(easing) {
30341 var defaultClass = Ext.fx.easing.EaseOut;
30342
30343 return {
30344 x: Ext.factory(easing, defaultClass),
30345 y: Ext.factory(easing, defaultClass)
30346 };
30347 },
30348
30349 updateBounceEasing: function(easing) {
30350 this.getTranslatable().setEasingX(easing.x).setEasingY(easing.y);
30351 },
30352
30353 /**
30354 * @private
30355 * @return {Object}
30356 */
30357 applySlotSnapEasing: function(easing) {
30358 var defaultClass = Ext.fx.easing.EaseOut;
30359
30360 return {
30361 x: Ext.factory(easing, defaultClass),
30362 y: Ext.factory(easing, defaultClass)
30363 };
30364 },
30365
30366 /**
30367 * @private
30368 * @return {Object}
30369 */
30370 getMinPosition: function() {
30371 var minPosition = this.minPosition;
30372
30373 if (!minPosition) {
30374 this.minPosition = minPosition = {
30375 x: 0,
30376 y: 0
30377 };
30378
30379 this.fireEvent('minpositionchange', this, minPosition);
30380 }
30381
30382 return minPosition;
30383 },
30384
30385 /**
30386 * @private
30387 * @return {Object}
30388 */
30389 getMaxPosition: function() {
30390 var maxPosition = this.maxPosition,
30391 size, containerSize;
30392
30393 if (!maxPosition) {
30394 size = this.getSize();
30395 containerSize = this.getContainerSize();
30396
30397 this.maxPosition = maxPosition = {
30398 x: Math.max(0, size.x - containerSize.x),
30399 y: Math.max(0, size.y - containerSize.y)
30400 };
30401
30402 this.fireEvent('maxpositionchange', this, maxPosition);
30403 }
30404
30405 return maxPosition;
30406 },
30407
30408 /**
30409 * @private
30410 */
30411 refreshMaxPosition: function() {
30412 this.maxPosition = null;
30413 this.getMaxPosition();
30414 },
30415
30416 /**
30417 * @private
30418 * @return {Object}
30419 */
30420 applyContainerSize: function(size) {
30421 var containerDom = this.getContainer().dom,
30422 x, y;
30423
30424 if (!containerDom) {
30425 return;
30426 }
30427
30428 this.givenContainerSize = size;
30429
30430 if (size === 'auto') {
30431 x = containerDom.offsetWidth;
30432 y = containerDom.offsetHeight;
30433 }
30434 else {
30435 x = size.x;
30436 y = size.y;
30437 }
30438
30439 return {
30440 x: x,
30441 y: y
30442 };
30443 },
30444
30445 /**
30446 * @private
30447 * @param {String/Object} size
30448 * @return {Object}
30449 */
30450 applySize: function(size) {
30451 var dom = this.getElement().dom,
30452 x, y;
30453
30454 if (!dom) {
30455 return;
30456 }
30457
30458 this.givenSize = size;
30459
30460 if (size === 'auto') {
30461 x = dom.offsetWidth;
30462 y = dom.offsetHeight;
30463 }
30464 else if (typeof size == 'number') {
30465 x = size;
30466 y = size;
30467 }
30468 else {
30469 x = size.x;
30470 y = size.y;
30471 }
30472
30473 return {
30474 x: x,
30475 y: y
30476 };
30477 },
30478
30479 /**
30480 * @private
30481 */
30482 updateAutoRefresh: function(autoRefresh) {
30483 this.getElement().toggleListener(autoRefresh, 'resize', 'onElementResize', this);
30484 this.getContainer().toggleListener(autoRefresh, 'resize', 'onContainerResize', this);
30485 },
30486
30487 applySlotSnapSize: function(snapSize) {
30488 if (typeof snapSize == 'number') {
30489 return {
30490 x: snapSize,
30491 y: snapSize
30492 };
30493 }
30494
30495 return snapSize;
30496 },
30497
30498 applySlotSnapOffset: function(snapOffset) {
30499 if (typeof snapOffset == 'number') {
30500 return {
30501 x: snapOffset,
30502 y: snapOffset
30503 };
30504 }
30505
30506 return snapOffset;
30507 },
30508
30509 /**
30510 * @private
30511 * Returns the container for this scroller
30512 */
30513 getContainer: function() {
30514 var container = this.container,
30515 element;
30516
30517 if (!container) {
30518 element = this.getElement().getParent();
30519 this.container = container = this.FixedHBoxStretching ? element.getParent() : element;
30520 //<debug error>
30521 if (!container) {
30522 Ext.Logger.error("Making an element scrollable that doesn't have any container");
30523 }
30524 //</debug>
30525 container.addCls(this.containerCls);
30526 }
30527
30528 return container;
30529 },
30530
30531 /**
30532 * @private
30533 * @return {Ext.scroll.Scroller} this
30534 * @chainable
30535 */
30536 refresh: function() {
30537 this.stopAnimation();
30538
30539 this.getTranslatable().refresh();
30540 this.setSize(this.givenSize);
30541 this.setContainerSize(this.givenContainerSize);
30542 this.setDirection(this.givenDirection);
30543
30544 this.fireEvent('refresh', this);
30545
30546 return this;
30547 },
30548
30549 onElementResize: function(element, info) {
30550 this.setSize({
30551 x: info.width,
30552 y: info.height
30553 });
30554
30555 this.refresh();
30556 },
30557
30558 onContainerResize: function(container, info) {
30559 this.setContainerSize({
30560 x: info.width,
30561 y: info.height
30562 });
30563
30564 this.refresh();
30565 },
30566
30567 /**
30568 * Scrolls to the given location.
30569 *
30570 * @param {Number} x The scroll position on the x axis.
30571 * @param {Number} y The scroll position on the y axis.
30572 * @param {Boolean/Object} animation (optional) Whether or not to animate the scrolling to the new position.
30573 *
30574 * @return {Ext.scroll.Scroller} this
30575 * @chainable
30576 */
30577 scrollTo: function(x, y, animation) {
30578 if (this.isDestroyed) {
30579 return this;
30580 }
30581
30582
30583 var translatable = this.getTranslatable(),
30584 position = this.position,
30585 positionChanged = false,
30586 translationX, translationY;
30587
30588 if (this.isAxisEnabled('x')) {
30589 if (isNaN(x) || typeof x != 'number') {
30590 x = position.x;
30591 }
30592 else {
30593 if (position.x !== x) {
30594 position.x = x;
30595 positionChanged = true;
30596 }
30597 }
30598
30599 translationX = -x;
30600 }
30601
30602 if (this.isAxisEnabled('y')) {
30603 if (isNaN(y) || typeof y != 'number') {
30604 y = position.y;
30605 }
30606 else {
30607 if (position.y !== y) {
30608 position.y = y;
30609 positionChanged = true;
30610 }
30611 }
30612
30613 translationY = -y;
30614 }
30615
30616 if (positionChanged) {
30617 if (animation !== undefined && animation !== false) {
30618 translatable.translateAnimated(translationX, translationY, animation);
30619 }
30620 else {
30621 this.fireEvent('scroll', this, position.x, position.y);
30622 translatable.translate(translationX, translationY);
30623 }
30624 }
30625
30626 return this;
30627 },
30628
30629 /**
30630 * @private
30631 * @return {Ext.scroll.Scroller} this
30632 * @chainable
30633 */
30634 scrollToTop: function(animation) {
30635 var initialOffset = this.getInitialOffset();
30636
30637 return this.scrollTo(initialOffset.x, initialOffset.y, animation);
30638 },
30639
30640 /**
30641 * Scrolls to the end of the scrollable view.
30642 * @return {Ext.scroll.Scroller} this
30643 * @chainable
30644 */
30645 scrollToEnd: function(animation) {
30646 var size = this.getSize(),
30647 cntSize = this.getContainerSize();
30648
30649 return this.scrollTo(size.x - cntSize.x, size.y - cntSize.y, animation);
30650 },
30651
30652 /**
30653 * Change the scroll offset by the given amount.
30654 * @param {Number} x The offset to scroll by on the x axis.
30655 * @param {Number} y The offset to scroll by on the y axis.
30656 * @param {Boolean/Object} animation (optional) Whether or not to animate the scrolling to the new position.
30657 * @return {Ext.scroll.Scroller} this
30658 * @chainable
30659 */
30660 scrollBy: function(x, y, animation) {
30661 var position = this.position;
30662
30663 x = (typeof x == 'number') ? x + position.x : null;
30664 y = (typeof y == 'number') ? y + position.y : null;
30665
30666 return this.scrollTo(x, y, animation);
30667 },
30668
30669 /**
30670 * @private
30671 */
30672 onTouchStart: function() {
30673 this.isTouching = true;
30674 this.stopAnimation();
30675 },
30676
30677 /**
30678 * @private
30679 */
30680 onTouchEnd: function() {
30681 var position = this.position;
30682
30683 this.isTouching = false;
30684
30685 if (!this.isDragging && this.snapToSlot()) {
30686 this.fireEvent('scrollstart', this, position.x, position.y);
30687 }
30688 },
30689
30690 /**
30691 * @private
30692 */
30693 onDragStart: function(e) {
30694 var direction = this.getDirection(),
30695 absDeltaX = e.absDeltaX,
30696 absDeltaY = e.absDeltaY,
30697 directionLock = this.getDirectionLock(),
30698 startPosition = this.startPosition,
30699 flickStartPosition = this.flickStartPosition,
30700 flickStartTime = this.flickStartTime,
30701 lastDragPosition = this.lastDragPosition,
30702 currentPosition = this.position,
30703 dragDirection = this.dragDirection,
30704 x = currentPosition.x,
30705 y = currentPosition.y,
30706 now = Ext.Date.now();
30707
30708 this.isDragging = true;
30709
30710 if (directionLock && direction !== 'both') {
30711 if ((direction === 'horizontal' && absDeltaX > absDeltaY)
30712 || (direction === 'vertical' && absDeltaY > absDeltaX)) {
30713 e.stopPropagation();
30714 }
30715 else {
30716 this.isDragging = false;
30717 return;
30718 }
30719 }
30720
30721 lastDragPosition.x = x;
30722 lastDragPosition.y = y;
30723
30724 flickStartPosition.x = x;
30725 flickStartPosition.y = y;
30726
30727 startPosition.x = x;
30728 startPosition.y = y;
30729
30730 flickStartTime.x = now;
30731 flickStartTime.y = now;
30732
30733 dragDirection.x = 0;
30734 dragDirection.y = 0;
30735
30736 this.dragStartTime = now;
30737
30738 this.isDragging = true;
30739
30740 this.fireEvent('scrollstart', this, x, y);
30741 },
30742
30743 /**
30744 * @private
30745 */
30746 onAxisDrag: function(axis, delta) {
30747 if (!this.isAxisEnabled(axis)) {
30748 return;
30749 }
30750
30751 var flickStartPosition = this.flickStartPosition,
30752 flickStartTime = this.flickStartTime,
30753 lastDragPosition = this.lastDragPosition,
30754 dragDirection = this.dragDirection,
30755 old = this.position[axis],
30756 min = this.getMinPosition()[axis],
30757 max = this.getMaxPosition()[axis],
30758 start = this.startPosition[axis],
30759 last = lastDragPosition[axis],
30760 current = start - delta,
30761 lastDirection = dragDirection[axis],
30762 restrictFactor = this.getOutOfBoundRestrictFactor(),
30763 startMomentumResetTime = this.getStartMomentumResetTime(),
30764 now = Ext.Date.now(),
30765 distance;
30766
30767 if (current < min) {
30768 current *= restrictFactor;
30769 }
30770 else if (current > max) {
30771 distance = current - max;
30772 current = max + distance * restrictFactor;
30773 }
30774
30775 if (current > last) {
30776 dragDirection[axis] = 1;
30777 }
30778 else if (current < last) {
30779 dragDirection[axis] = -1;
30780 }
30781
30782 if ((lastDirection !== 0 && (dragDirection[axis] !== lastDirection))
30783 || (now - flickStartTime[axis]) > startMomentumResetTime) {
30784 flickStartPosition[axis] = old;
30785 flickStartTime[axis] = now;
30786 }
30787
30788 lastDragPosition[axis] = current;
30789 },
30790
30791 /**
30792 * @private
30793 */
30794 onDrag: function(e) {
30795 if (!this.isDragging) {
30796 return;
30797 }
30798
30799 var lastDragPosition = this.lastDragPosition;
30800
30801 this.onAxisDrag('x', e.deltaX);
30802 this.onAxisDrag('y', e.deltaY);
30803
30804 this.scrollTo(lastDragPosition.x, lastDragPosition.y);
30805 },
30806
30807 /**
30808 * @private
30809 */
30810 onDragEnd: function(e) {
30811 var easingX, easingY;
30812
30813 if (!this.isDragging) {
30814 return;
30815 }
30816
30817 this.dragEndTime = Ext.Date.now();
30818
30819 this.onDrag(e);
30820
30821 this.isDragging = false;
30822
30823 easingX = this.getAnimationEasing('x', e);
30824 easingY = this.getAnimationEasing('y', e);
30825
30826 if (easingX || easingY) {
30827 this.getTranslatable().animate(easingX, easingY);
30828 }
30829 else {
30830 this.onScrollEnd();
30831 }
30832 },
30833
30834 /**
30835 * @private
30836 */
30837 getAnimationEasing: function(axis, e) {
30838 if (!this.isAxisEnabled(axis)) {
30839 return null;
30840 }
30841
30842 var currentPosition = this.position[axis],
30843 minPosition = this.getMinPosition()[axis],
30844 maxPosition = this.getMaxPosition()[axis],
30845 maxAbsVelocity = this.getMaxAbsoluteVelocity(),
30846 boundValue = null,
30847 dragEndTime = this.dragEndTime,
30848 velocity = e.flick.velocity[axis],
30849 easing;
30850
30851 if (currentPosition < minPosition) {
30852 boundValue = minPosition;
30853 }
30854 else if (currentPosition > maxPosition) {
30855 boundValue = maxPosition;
30856 }
30857
30858 // Out of bound, to be pulled back
30859 if (boundValue !== null) {
30860 easing = this.getBounceEasing()[axis];
30861 easing.setConfig({
30862 startTime: dragEndTime,
30863 startValue: -currentPosition,
30864 endValue: -boundValue
30865 });
30866
30867 return easing;
30868 }
30869
30870 if (velocity === 0) {
30871 return null;
30872 }
30873
30874 if (velocity < -maxAbsVelocity) {
30875 velocity = -maxAbsVelocity;
30876 }
30877 else if (velocity > maxAbsVelocity) {
30878 velocity = maxAbsVelocity;
30879 }
30880
30881 if (Ext.browser.is.IE) {
30882 velocity *= 2;
30883 }
30884
30885 easing = this.getMomentumEasing()[axis];
30886 easing.setConfig({
30887 startTime: dragEndTime,
30888 startValue: -currentPosition,
30889 startVelocity: velocity * 1.5,
30890 minMomentumValue: -maxPosition,
30891 maxMomentumValue: 0
30892 });
30893
30894 return easing;
30895 },
30896
30897 /**
30898 * @private
30899 */
30900 onAnimationFrame: function(translatable, x, y) {
30901 var position = this.position;
30902
30903 position.x = -x;
30904 position.y = -y;
30905
30906 this.fireEvent('scroll', this, position.x, position.y);
30907 },
30908
30909 /**
30910 * @private
30911 */
30912 onAnimationEnd: function() {
30913 this.snapToBoundary();
30914 this.onScrollEnd();
30915 },
30916
30917 /**
30918 * @private
30919 * Stops the animation of the scroller at any time.
30920 */
30921 stopAnimation: function() {
30922 this.getTranslatable().stopAnimation();
30923 },
30924
30925 /**
30926 * @private
30927 */
30928 onScrollEnd: function() {
30929 var position = this.position;
30930
30931 if (this.isTouching || !this.snapToSlot()) {
30932 this.fireEvent('scrollend', this, position.x, position.y);
30933 }
30934 },
30935
30936 /**
30937 * @private
30938 * @return {Boolean}
30939 */
30940 snapToSlot: function() {
30941 var snapX = this.getSnapPosition('x'),
30942 snapY = this.getSnapPosition('y'),
30943 easing = this.getSlotSnapEasing();
30944
30945 if (snapX !== null || snapY !== null) {
30946 this.scrollTo(snapX, snapY, {
30947 easingX: easing.x,
30948 easingY: easing.y
30949 });
30950
30951 return true;
30952 }
30953
30954 return false;
30955 },
30956
30957 /**
30958 * @private
30959 * @return {Number/null}
30960 */
30961 getSnapPosition: function(axis) {
30962 var snapSize = this.getSlotSnapSize()[axis],
30963 snapPosition = null,
30964 position, snapOffset, maxPosition, mod;
30965
30966 if (snapSize !== 0 && this.isAxisEnabled(axis)) {
30967 position = this.position[axis];
30968 snapOffset = this.getSlotSnapOffset()[axis];
30969 maxPosition = this.getMaxPosition()[axis];
30970
30971 mod = Math.floor((position - snapOffset) % snapSize);
30972
30973 if (mod !== 0) {
30974 if (position !== maxPosition) {
30975 if (Math.abs(mod) > snapSize / 2) {
30976 snapPosition = Math.min(maxPosition, position + ((mod > 0) ? snapSize - mod : mod - snapSize));
30977 }
30978 else {
30979 snapPosition = position - mod;
30980 }
30981 }
30982 else {
30983 snapPosition = position - mod;
30984 }
30985 }
30986 }
30987
30988 return snapPosition;
30989 },
30990
30991 /**
30992 * @private
30993 */
30994 snapToBoundary: function() {
30995 var position = this.position,
30996 minPosition = this.getMinPosition(),
30997 maxPosition = this.getMaxPosition(),
30998 minX = minPosition.x,
30999 minY = minPosition.y,
31000 maxX = maxPosition.x,
31001 maxY = maxPosition.y,
31002 x = Math.round(position.x),
31003 y = Math.round(position.y);
31004
31005 if (x < minX) {
31006 x = minX;
31007 }
31008 else if (x > maxX) {
31009 x = maxX;
31010 }
31011
31012 if (y < minY) {
31013 y = minY;
31014 }
31015 else if (y > maxY) {
31016 y = maxY;
31017 }
31018
31019 this.scrollTo(x, y);
31020 },
31021
31022 destroy: function() {
31023 var element = this.getElement(),
31024 sizeMonitors = this.sizeMonitors,
31025 container;
31026
31027 if (sizeMonitors) {
31028 sizeMonitors.element.destroy();
31029 sizeMonitors.container.destroy();
31030 }
31031
31032 if (element && !element.isDestroyed) {
31033 element.removeCls(this.cls);
31034 container = this.getContainer();
31035 if (container && !container.isDestroyed) {
31036 container.removeCls(this.containerCls);
31037 }
31038 }
31039
31040 Ext.destroy(this.getTranslatable());
31041
31042 this.callParent(arguments);
31043 }
31044
31045 }, function() {
31046 });
31047
31048 (function() {
31049 var lastTime = 0,
31050 vendors = ['ms', 'moz', 'webkit', 'o'],
31051 ln = vendors.length,
31052 i, vendor;
31053
31054 for (i = 0; i < ln && !window.requestAnimationFrame; ++i) {
31055 vendor = vendors[i];
31056 if (window[vendor + 'RequestAnimationFrame']) {
31057 window.requestAnimationFrame = window[vendor + 'RequestAnimationFrame'];
31058 window.cancelAnimationFrame = window[vendor + 'CancelAnimationFrame'] || window[vendor + 'CancelRequestAnimationFrame'];
31059 }
31060 }
31061
31062 if (!window.Ext) {
31063 window.Ext = {};
31064 }
31065 Ext.performance = {};
31066
31067 if (window.performance && window.performance.now) {
31068 Ext.performance.now = function() {
31069 return window.performance.now();
31070 }
31071 }
31072 else {
31073 Ext.performance.now = function() {
31074 return Date.now();
31075 }
31076 }
31077
31078 if (!window.requestAnimationFrame) {
31079 window.requestAnimationFrame = function(callback) {
31080 var currTime = Ext.performance.now(),
31081 timeToCall = Math.max(0, 16 - (currTime - lastTime)),
31082 id = window.setTimeout(function() {
31083 callback(currTime + timeToCall);
31084 }, timeToCall);
31085 lastTime = currTime + timeToCall;
31086 return id;
31087 };
31088 }
31089 else {
31090 Ext.trueRequestAnimationFrames = true;
31091 }
31092
31093 if (!window.cancelAnimationFrame) {
31094 window.cancelAnimationFrame = function(id) {
31095 clearTimeout(id);
31096 };
31097 }
31098 }());
31099
31100 (function(global) {
31101
31102 /**
31103 * @private
31104 */
31105 Ext.define('Ext.AnimationQueue', {
31106 singleton: true,
31107
31108 constructor: function() {
31109 var bind = Ext.Function.bind;
31110
31111 this.queue = [];
31112 this.taskQueue = [];
31113 this.runningQueue = [];
31114 this.idleQueue = [];
31115 this.isRunning = false;
31116 this.isIdle = true;
31117
31118 this.run = bind(this.run, this);
31119 this.whenIdle = bind(this.whenIdle, this);
31120 this.processIdleQueueItem = bind(this.processIdleQueueItem, this);
31121 this.processTaskQueueItem = bind(this.processTaskQueueItem, this);
31122
31123
31124 // iOS has a nasty bug which causes pending requestAnimationFrame to not release
31125 // the callback when the WebView is switched back and forth from / to being background process
31126 // We use a watchdog timer to workaround this, and restore the pending state correctly if this happens
31127 // This timer has to be set as an interval from the very beginning and we have to keep it running for
31128 // as long as the app lives, setting it later doesn't seem to work
31129 if (Ext.os.is.iOS) {
31130 setInterval(this.watch, 500);
31131 }
31132 },
31133
31134 /**
31135 *
31136 * @param {Function} fn
31137 * @param {Object} [scope]
31138 * @param {Object} [args]
31139 */
31140 start: function(fn, scope, args) {
31141 this.queue.push(arguments);
31142
31143 if (!this.isRunning) {
31144 if (this.hasOwnProperty('idleTimer')) {
31145 clearTimeout(this.idleTimer);
31146 delete this.idleTimer;
31147 }
31148
31149 if (this.hasOwnProperty('idleQueueTimer')) {
31150 clearTimeout(this.idleQueueTimer);
31151 delete this.idleQueueTimer;
31152 }
31153
31154 this.isIdle = false;
31155 this.isRunning = true;
31156 //<debug>
31157 this.startCountTime = Ext.performance.now();
31158 this.count = 0;
31159 //</debug>
31160 this.doStart();
31161 }
31162 },
31163
31164 watch: function() {
31165 if (this.isRunning && Date.now() - this.lastRunTime >= 500) {
31166 this.run();
31167 }
31168 },
31169
31170 run: function() {
31171 if (!this.isRunning) {
31172 return;
31173 }
31174
31175 var queue = this.runningQueue,
31176 i, ln;
31177
31178 this.lastRunTime = Date.now();
31179 this.frameStartTime = Ext.performance.now();
31180
31181 queue.push.apply(queue, this.queue);
31182
31183 for (i = 0, ln = queue.length; i < ln; i++) {
31184 this.invoke(queue[i]);
31185 }
31186
31187 queue.length = 0;
31188
31189 //<debug>
31190 var now = this.frameStartTime,
31191 startCountTime = this.startCountTime,
31192 elapse = now - startCountTime,
31193 count = ++this.count;
31194
31195 if (elapse >= 200) {
31196 this.onFpsChanged(count * 1000 / elapse, count, elapse);
31197 this.startCountTime = now;
31198 this.count = 0;
31199 }
31200 //</debug>
31201
31202 this.doIterate();
31203 },
31204
31205 //<debug>
31206 onFpsChanged: Ext.emptyFn,
31207
31208 onStop: Ext.emptyFn,
31209 //</debug>
31210
31211 doStart: function() {
31212 this.animationFrameId = requestAnimationFrame(this.run);
31213 this.lastRunTime = Date.now();
31214 },
31215
31216 doIterate: function() {
31217 this.animationFrameId = requestAnimationFrame(this.run);
31218 },
31219
31220 doStop: function() {
31221 cancelAnimationFrame(this.animationFrameId);
31222 },
31223
31224 /**
31225 *
31226 * @param {Function} fn
31227 * @param {Object} [scope]
31228 * @param {Object} [args]
31229 */
31230 stop: function(fn, scope, args) {
31231 if (!this.isRunning) {
31232 return;
31233 }
31234
31235 var queue = this.queue,
31236 ln = queue.length,
31237 i, item;
31238
31239 for (i = 0; i < ln; i++) {
31240 item = queue[i];
31241 if (item[0] === fn && item[1] === scope && item[2] === args) {
31242 queue.splice(i, 1);
31243 i--;
31244 ln--;
31245 }
31246 }
31247
31248 if (ln === 0) {
31249 this.doStop();
31250 //<debug>
31251 this.onStop();
31252 //</debug>
31253 this.isRunning = false;
31254
31255 this.idleTimer = setTimeout(this.whenIdle, 100);
31256 }
31257 },
31258
31259 onIdle: function(fn, scope, args) {
31260 var listeners = this.idleQueue,
31261 i, ln, listener;
31262
31263 for (i = 0, ln = listeners.length; i < ln; i++) {
31264 listener = listeners[i];
31265 if (fn === listener[0] && scope === listener[1] && args === listener[2]) {
31266 return;
31267 }
31268 }
31269
31270 listeners.push(arguments);
31271
31272 if (this.isIdle) {
31273 this.processIdleQueue();
31274 }
31275 },
31276
31277 unIdle: function(fn, scope, args) {
31278 var listeners = this.idleQueue,
31279 i, ln, listener;
31280
31281 for (i = 0, ln = listeners.length; i < ln; i++) {
31282 listener = listeners[i];
31283 if (fn === listener[0] && scope === listener[1] && args === listener[2]) {
31284 listeners.splice(i, 1);
31285 return true;
31286 }
31287 }
31288
31289 return false;
31290 },
31291
31292 queueTask: function(fn, scope, args) {
31293 this.taskQueue.push(arguments);
31294 this.processTaskQueue();
31295 },
31296
31297 dequeueTask: function(fn, scope, args) {
31298 var listeners = this.taskQueue,
31299 i, ln, listener;
31300
31301 for (i = 0, ln = listeners.length; i < ln; i++) {
31302 listener = listeners[i];
31303 if (fn === listener[0] && scope === listener[1] && args === listener[2]) {
31304 listeners.splice(i, 1);
31305 i--;
31306 ln--;
31307 }
31308 }
31309 },
31310
31311 invoke: function(listener) {
31312 var fn = listener[0],
31313 scope = listener[1],
31314 args = listener[2];
31315
31316 fn = (typeof fn == 'string' ? scope[fn] : fn);
31317
31318 if (Ext.isArray(args)) {
31319 fn.apply(scope, args);
31320 }
31321 else {
31322 fn.call(scope, args);
31323 }
31324 },
31325
31326 whenIdle: function() {
31327 this.isIdle = true;
31328 this.processIdleQueue();
31329 },
31330
31331 processIdleQueue: function() {
31332 if (!this.hasOwnProperty('idleQueueTimer')) {
31333 this.idleQueueTimer = setTimeout(this.processIdleQueueItem, 1);
31334 }
31335 },
31336
31337 processIdleQueueItem: function() {
31338 delete this.idleQueueTimer;
31339
31340 if (!this.isIdle) {
31341 return;
31342 }
31343
31344 var listeners = this.idleQueue,
31345 listener;
31346
31347 if (listeners.length > 0) {
31348 listener = listeners.shift();
31349 this.invoke(listener);
31350 this.processIdleQueue();
31351 }
31352 },
31353
31354 processTaskQueue: function() {
31355 if (!this.hasOwnProperty('taskQueueTimer')) {
31356 this.taskQueueTimer = setTimeout(this.processTaskQueueItem, 15);
31357 }
31358 },
31359
31360 processTaskQueueItem: function() {
31361 delete this.taskQueueTimer;
31362
31363 var listeners = this.taskQueue,
31364 listener;
31365
31366 if (listeners.length > 0) {
31367 listener = listeners.shift();
31368 this.invoke(listener);
31369 this.processTaskQueue();
31370 }
31371 },
31372
31373 showFps: function() {
31374 if (!Ext.trueRequestAnimationFrames) {
31375 alert("This browser does not support requestAnimationFrame. The FPS listed will not be accurate");
31376 }
31377 Ext.onReady(function() {
31378 Ext.Viewport.add([{
31379 xtype: 'component',
31380 bottom: 50,
31381 left: 0,
31382 width: 50,
31383 height: 20,
31384 html: 'Average',
31385 style: 'background-color: black; color: white; text-align: center; line-height: 20px; font-size: 8px;'
31386 },
31387 {
31388 id: '__averageFps',
31389 xtype: 'component',
31390 bottom: 0,
31391 left: 0,
31392 width: 50,
31393 height: 50,
31394 html: '0',
31395 style: 'background-color: red; color: white; text-align: center; line-height: 50px;'
31396 },
31397 {
31398 xtype: 'component',
31399 bottom: 50,
31400 left: 50,
31401 width: 50,
31402 height: 20,
31403 html: 'Min (Last 1k)',
31404 style: 'background-color: black; color: white; text-align: center; line-height: 20px; font-size: 8px;'
31405 },
31406 {
31407 id: '__minFps',
31408 xtype: 'component',
31409 bottom: 0,
31410 left: 50,
31411 width: 50,
31412 height: 50,
31413 html: '0',
31414 style: 'background-color: orange; color: white; text-align: center; line-height: 50px;'
31415 },
31416 {
31417 xtype: 'component',
31418 bottom: 50,
31419 left: 100,
31420 width: 50,
31421 height: 20,
31422 html: 'Max (Last 1k)',
31423 style: 'background-color: black; color: white; text-align: center; line-height: 20px; font-size: 8px;'
31424 },
31425 {
31426 id: '__maxFps',
31427 xtype: 'component',
31428 bottom: 0,
31429 left: 100,
31430 width: 50,
31431 height: 50,
31432 html: '0',
31433 style: 'background-color: yellow; color: black; text-align: center; line-height: 50px;'
31434 },
31435 {
31436 xtype: 'component',
31437 bottom: 50,
31438 left: 150,
31439 width: 50,
31440 height: 20,
31441 html: 'Current',
31442 style: 'background-color: black; color: white; text-align: center; line-height: 20px; font-size: 8px;'
31443 },
31444 {
31445 id: '__currentFps',
31446 xtype: 'component',
31447 bottom: 0,
31448 left: 150,
31449 width: 50,
31450 height: 50,
31451 html: '0',
31452 style: 'background-color: green; color: white; text-align: center; line-height: 50px;'
31453 }
31454 ]);
31455 Ext.AnimationQueue.resetFps();
31456 });
31457
31458 },
31459
31460 resetFps: function() {
31461 var currentFps = Ext.getCmp('__currentFps'),
31462 averageFps = Ext.getCmp('__averageFps'),
31463 minFps = Ext.getCmp('__minFps'),
31464 maxFps = Ext.getCmp('__maxFps'),
31465 min = 1000,
31466 max = 0,
31467 count = 0,
31468 sum = 0;
31469
31470 Ext.AnimationQueue.onFpsChanged = function(fps) {
31471 count++;
31472
31473 if (!(count % 10)) {
31474 min = 1000;
31475 max = 0;
31476 }
31477
31478 sum += fps;
31479 min = Math.min(min, fps);
31480 max = Math.max(max, fps);
31481 currentFps.setHtml(Math.round(fps));
31482 averageFps.setHtml(Math.round(sum / count));
31483 minFps.setHtml(Math.round(min));
31484 maxFps.setHtml(Math.round(max));
31485 };
31486 }
31487 }, function() {
31488 /*
31489 Global FPS indicator. Add ?showfps to use in any application. Note that this REQUIRES true requestAnimationFrame
31490 to be accurate.
31491 */
31492 //<debug>
31493 var paramsString = window.location.search.substr(1),
31494 paramsArray = paramsString.split("&");
31495
31496 if (paramsArray.indexOf("showfps") !== -1) {
31497 Ext.AnimationQueue.showFps();
31498 }
31499 //</debug>
31500
31501 });
31502
31503 })(this);
31504
31505 /**
31506 * @private
31507 * Handle batch read / write of DOMs, currently used in SizeMonitor + PaintMonitor
31508 */
31509 Ext.define('Ext.TaskQueue', {
31510
31511
31512 singleton: true,
31513
31514 pending: false,
31515
31516 mode: true,
31517
31518 constructor: function() {
31519 this.readQueue = [];
31520 this.writeQueue = [];
31521
31522 this.run = Ext.Function.bind(this.run, this);
31523 this.watch = Ext.Function.bind(this.watch, this);
31524
31525 // iOS has a nasty bug which causes pending requestAnimationFrame to not release
31526 // the callback when the WebView is switched back and forth from / to being background process
31527 // We use a watchdog timer to workaround this, and restore the pending state correctly if this happens
31528 // This timer has to be set as an interval from the very beginning and we have to keep it running for
31529 // as long as the app lives, setting it later doesn't seem to work
31530 if (Ext.os.is.iOS) {
31531 setInterval(this.watch, 500);
31532 }
31533 },
31534
31535 requestRead: function(fn, scope, args) {
31536 this.request(true);
31537 this.readQueue.push(arguments);
31538 },
31539
31540 requestWrite: function(fn, scope, args) {
31541 this.request(false);
31542 this.writeQueue.push(arguments);
31543 },
31544
31545 request: function(mode) {
31546 if (!this.pending) {
31547 this.pendingTime = Date.now();
31548 this.pending = true;
31549 this.mode = mode;
31550 if (mode) {
31551 setTimeout(this.run, 1);
31552 } else {
31553 requestAnimationFrame(this.run);
31554 }
31555 }
31556 },
31557
31558 watch: function() {
31559 if (this.pending && Date.now() - this.pendingTime >= 500) {
31560 this.run();
31561 }
31562 },
31563
31564 run: function() {
31565 this.pending = false;
31566
31567 var readQueue = this.readQueue,
31568 writeQueue = this.writeQueue,
31569 request = null,
31570 queue;
31571
31572 if (this.mode) {
31573 queue = readQueue;
31574
31575 if (writeQueue.length > 0) {
31576 request = false;
31577 }
31578 }
31579 else {
31580 queue = writeQueue;
31581
31582 if (readQueue.length > 0) {
31583 request = true;
31584 }
31585 }
31586
31587 var tasks = queue.slice(),
31588 i, ln, task, fn, scope;
31589
31590 queue.length = 0;
31591
31592 for (i = 0, ln = tasks.length; i < ln; i++) {
31593 task = tasks[i];
31594 fn = task[0];
31595 scope = task[1];
31596
31597 if (typeof fn == 'string') {
31598 fn = scope[fn];
31599 }
31600
31601 if (task.length > 2) {
31602 fn.apply(scope, task[2]);
31603 }
31604 else {
31605 fn.call(scope);
31606 }
31607 }
31608
31609 tasks.length = 0;
31610
31611 if (request !== null) {
31612 this.request(request);
31613 }
31614 }
31615 });
31616
31617 /**
31618 * @private
31619 */
31620 Ext.define('Ext.scroll.indicator.Abstract', {
31621 extend: Ext.Component ,
31622
31623
31624
31625
31626
31627 config: {
31628 baseCls: 'x-scroll-indicator',
31629
31630 axis: 'x',
31631
31632 value: null,
31633
31634 length: null,
31635
31636 minLength: 6,
31637
31638 hidden: true,
31639
31640 ui: 'dark',
31641
31642 /**
31643 * @cfg {Boolean} [autoHide=true] Set to `false` to always show the indicator for this axis.
31644 */
31645 autoHide : true
31646 },
31647
31648 cachedConfig: {
31649 ratio: 1,
31650
31651 barCls: 'x-scroll-bar',
31652
31653 active: true
31654 },
31655
31656 barElement: null,
31657
31658 barLength: 0,
31659
31660 gapLength: 0,
31661
31662 getElementConfig: function() {
31663 return {
31664 reference: 'barElement',
31665 children: [this.callParent()]
31666 };
31667 },
31668
31669 applyRatio: function(ratio) {
31670 if (isNaN(ratio) || ratio > 1) {
31671 ratio = 1;
31672 }
31673
31674 return ratio;
31675 },
31676
31677 refresh: function() {
31678 var bar = this.barElement,
31679 barDom = bar.dom,
31680 ratio = this.getRatio(),
31681 axis = this.getAxis(),
31682 barLength = (axis === 'x') ? barDom.offsetWidth : barDom.offsetHeight,
31683 length = barLength * ratio;
31684
31685 this.barLength = barLength;
31686
31687 this.gapLength = barLength - length;
31688
31689 this.setLength(length);
31690
31691 this.updateValue(this.getValue());
31692 },
31693
31694 updateBarCls: function(barCls) {
31695 this.barElement.addCls(barCls);
31696 },
31697
31698 updateAxis: function(axis) {
31699 this.element.addCls(this.getBaseCls(), null, axis);
31700 this.barElement.addCls(this.getBarCls(), null, axis);
31701 },
31702
31703 updateValue: function(value) {
31704 var barLength = this.barLength,
31705 gapLength = this.gapLength,
31706 length = this.getLength(),
31707 newLength, offset, extra;
31708
31709 if (value <= 0) {
31710 offset = 0;
31711 this.updateLength(this.applyLength(length + value * barLength));
31712 }
31713 else if (value >= 1) {
31714 extra = Math.round((value - 1) * barLength);
31715 newLength = this.applyLength(length - extra);
31716 extra = length - newLength;
31717 this.updateLength(newLength);
31718 offset = gapLength + extra;
31719 }
31720 else {
31721 offset = gapLength * value;
31722 }
31723
31724 this.setOffset(offset);
31725 },
31726
31727 updateActive: function(active) {
31728 this.barElement[active ? 'addCls' : 'removeCls']('active');
31729 },
31730
31731 doSetHidden: function(hidden) {
31732 var me = this;
31733
31734 if (hidden) {
31735 me.getAutoHide() && me.setOffset(-10000);
31736 } else {
31737 delete me.lastLength;
31738 delete me.lastOffset;
31739 me.updateValue(me.getValue());
31740 }
31741 },
31742
31743 applyLength: function(length) {
31744 return Math.max(this.getMinLength(), length);
31745 },
31746
31747 updateLength: function(length) {
31748 length = Math.round(length);
31749 if (this.lastLength === length) {
31750 return;
31751 }
31752 this.lastLength = length;
31753 Ext.TaskQueue.requestWrite('doUpdateLength', this, [length]);
31754 },
31755
31756 doUpdateLength: function(length){
31757 if (!this.isDestroyed) {
31758 var axis = this.getAxis(),
31759 element = this.element;
31760
31761 if (axis === 'x') {
31762 element.setWidth(length);
31763 }
31764 else {
31765 element.setHeight(length);
31766 }
31767 }
31768 },
31769
31770 setOffset: function(offset) {
31771 offset = Math.round(offset);
31772 if (this.lastOffset === offset || this.lastOffset === -10000) {
31773 return;
31774 }
31775 this.lastOffset = offset;
31776 Ext.TaskQueue.requestWrite('doSetOffset', this,[offset]);
31777 },
31778
31779 doSetOffset: function(offset) {
31780 if (!this.isDestroyed) {
31781 var axis = this.getAxis(),
31782 element = this.element;
31783
31784 if (axis === 'x') {
31785 element.translate(offset, 0);
31786 }
31787 else {
31788 element.translate(0, offset);
31789 }
31790 }
31791 }
31792 });
31793
31794 /**
31795 * @private
31796 */
31797 Ext.define('Ext.scroll.indicator.CssTransform', {
31798 extend: Ext.scroll.indicator.Abstract ,
31799
31800 config: {
31801 cls: 'csstransform'
31802 }
31803 });
31804
31805 /**
31806 * @private
31807 */
31808 Ext.define('Ext.scroll.indicator.ScrollPosition', {
31809 extend: Ext.scroll.indicator.Abstract ,
31810
31811 config: {
31812 cls: 'scrollposition'
31813 },
31814
31815 getElementConfig: function() {
31816 var config = this.callParent(arguments);
31817
31818 config.children.unshift({
31819 className: 'x-scroll-bar-stretcher'
31820 });
31821
31822 return config;
31823 },
31824
31825 updateValue: function(value) {
31826 if (this.gapLength === 0) {
31827 if (value >= 1) {
31828 value--;
31829 }
31830
31831 this.setOffset(this.barLength * value);
31832 }
31833 else {
31834 this.setOffset(this.gapLength * value);
31835 }
31836 },
31837
31838 doUpdateLength: function() {
31839 if (!this.isDestroyed) {
31840 var scrollOffset = this.barLength,
31841 element = this.element;
31842
31843 this.callParent(arguments);
31844
31845 if (this.getAxis() === 'x') {
31846 element.setLeft(scrollOffset);
31847 }
31848 else {
31849 element.setTop(scrollOffset);
31850 }
31851 }
31852 },
31853
31854 doSetOffset: function(offset) {
31855 if (!this.isDestroyed) {
31856 var barLength = this.barLength,
31857 minLength = this.getMinLength(),
31858 barDom = this.barElement.dom;
31859
31860 if (offset !== -10000) {
31861 offset = Math.min(barLength - minLength, Math.max(offset, minLength - this.getLength()));
31862 offset = barLength - offset;
31863 }
31864
31865 if (this.getAxis() === 'x') {
31866 barDom.scrollLeft = offset;
31867 }
31868 else {
31869 barDom.scrollTop = offset;
31870 }
31871 }
31872 }
31873 });
31874
31875 /**
31876 * @private
31877 */
31878 Ext.define('Ext.scroll.indicator.Rounded', {
31879 extend: Ext.scroll.indicator.Abstract ,
31880
31881 config: {
31882 cls: 'rounded'
31883 },
31884
31885 constructor: function() {
31886 this.callParent(arguments);
31887 this.transformPropertyName = Ext.browser.getVendorProperyName('transform');
31888 },
31889
31890 getElementConfig: function() {
31891 var config = this.callParent();
31892
31893 config.children[0].children = [
31894 {
31895 reference: 'startElement'
31896 },
31897 {
31898 reference: 'middleElement'
31899 },
31900 {
31901 reference: 'endElement'
31902 }
31903 ];
31904
31905 return config;
31906 },
31907
31908 refresh: function() {
31909 var axis = this.getAxis(),
31910 startElementDom = this.startElement.dom,
31911 endElementDom = this.endElement.dom,
31912 middleElement = this.middleElement,
31913 startElementLength, endElementLength;
31914
31915 if (axis === 'x') {
31916 startElementLength = startElementDom.offsetWidth;
31917 endElementLength = endElementDom.offsetWidth;
31918 middleElement.setLeft(startElementLength);
31919 }
31920 else {
31921 startElementLength = startElementDom.offsetHeight;
31922 endElementLength = endElementDom.offsetHeight;
31923 middleElement.setTop(startElementLength);
31924 }
31925
31926 this.startElementLength = startElementLength;
31927 this.endElementLength = endElementLength;
31928
31929 this.callParent();
31930 },
31931
31932 doUpdateLength: function(length) {
31933 if (!this.isDestroyed) {
31934 var axis = this.getAxis(),
31935 endElement = this.endElement,
31936 middleElementStyle = this.middleElement.dom.style,
31937 endElementLength = this.endElementLength,
31938 endElementOffset = length - endElementLength,
31939 middleElementLength = endElementOffset - this.startElementLength,
31940 transformPropertyName = this.transformPropertyName;
31941
31942 if (axis === 'x') {
31943 endElement.translate(endElementOffset, 0);
31944 middleElementStyle[transformPropertyName] = 'translate3d(0, 0, 0) scaleX(' + middleElementLength + ')';
31945 }
31946 else {
31947 endElement.translate(0, endElementOffset);
31948 middleElementStyle[transformPropertyName] = 'translate3d(0, 0, 0) scaleY(' + middleElementLength + ')';
31949 }
31950 }
31951 }
31952 });
31953
31954 /**
31955 * @private
31956 */
31957 Ext.define('Ext.scroll.Indicator', {
31958
31959
31960
31961
31962
31963
31964 alternateClassName: 'Ext.util.Indicator',
31965
31966 constructor: function(config) {
31967 var namespace = Ext.scroll.indicator;
31968
31969 switch (Ext.browser.getPreferredTranslationMethod(config)) {
31970 case 'scrollposition':
31971 return new namespace.ScrollPosition(config);
31972 case 'csstransform':
31973 if (Ext.browser.is.AndroidStock4) {
31974 return new namespace.CssTransform(config);
31975 } else {
31976 return new namespace.Rounded(config);
31977 }
31978 }
31979 }
31980 });
31981
31982 /**
31983 * This is a simple container that is used to compile content and a {@link Ext.scroll.View} instance. It also
31984 * provides scroll indicators.
31985 *
31986 * 99% of the time all you need to use in this class is {@link #getScroller}.
31987 *
31988 * This should never be extended.
31989 */
31990 Ext.define('Ext.scroll.View', {
31991 extend: Ext.Evented ,
31992
31993 alternateClassName: 'Ext.util.ScrollView',
31994
31995
31996
31997
31998
31999
32000 config: {
32001 /**
32002 * @cfg {String} indicatorsUi
32003 * The style of the indicators of this view. Available options are `dark` or `light`.
32004 */
32005 indicatorsUi: 'dark',
32006
32007 element: null,
32008
32009 scroller: {},
32010
32011 indicators: {
32012 x: {
32013 axis: 'x'
32014 },
32015 y: {
32016 axis: 'y'
32017 }
32018 },
32019
32020 indicatorsHidingDelay: 100,
32021
32022 cls: Ext.baseCSSPrefix + 'scroll-view'
32023 },
32024
32025 /**
32026 * @method getScroller
32027 * Returns the scroller instance in this view. Checkout the documentation of {@link Ext.scroll.Scroller} and
32028 * {@link Ext.Container#getScrollable} for more information.
32029 * @return {Ext.scroll.View} The scroller
32030 */
32031
32032 /**
32033 * @private
32034 */
32035 processConfig: function(config) {
32036 if (!config) {
32037 return null;
32038 }
32039
32040 if (typeof config == 'string') {
32041 config = {
32042 direction: config
32043 };
32044 }
32045
32046 config = Ext.merge({}, config);
32047
32048 var scrollerConfig = config.scroller,
32049 name;
32050
32051 if (!scrollerConfig) {
32052 config.scroller = scrollerConfig = {};
32053 }
32054
32055 for (name in config) {
32056 if (config.hasOwnProperty(name)) {
32057 if (!this.hasConfig(name)) {
32058 scrollerConfig[name] = config[name];
32059 delete config[name];
32060 }
32061 }
32062 }
32063
32064 return config;
32065 },
32066
32067 constructor: function(config) {
32068 config = this.processConfig(config);
32069
32070 this.useIndicators = { x: true, y: true };
32071
32072 this.doHideIndicators = Ext.Function.bind(this.doHideIndicators, this);
32073
32074 this.initConfig(config);
32075 },
32076
32077 setConfig: function(config) {
32078 return this.callParent([this.processConfig(config)]);
32079 },
32080
32081 updateIndicatorsUi: function(newUi) {
32082 var indicators = this.getIndicators();
32083 indicators.x.setUi(newUi);
32084 indicators.y.setUi(newUi);
32085 },
32086
32087 applyScroller: function(config, currentScroller) {
32088 return Ext.factory(config, Ext.scroll.Scroller, currentScroller);
32089 },
32090
32091 applyIndicators: function(config, indicators) {
32092 var defaultClass = Ext.scroll.Indicator,
32093 useIndicators = this.useIndicators;
32094
32095 if (!config) {
32096 config = {};
32097 }
32098
32099 if (!config.x) {
32100 useIndicators.x = false;
32101 config.x = {};
32102 }
32103
32104 if (!config.y) {
32105 useIndicators.y = false;
32106 config.y = {};
32107 }
32108
32109 return {
32110 x: Ext.factory(config.x, defaultClass, indicators && indicators.x),
32111 y: Ext.factory(config.y, defaultClass, indicators && indicators.y)
32112 };
32113 },
32114
32115 updateIndicators: function(indicators) {
32116 this.indicatorsGrid = Ext.Element.create({
32117 className: 'x-scroll-bar-grid-wrapper',
32118 children: [{
32119 className: 'x-scroll-bar-grid',
32120 children: [
32121 {
32122 children: [{}, {
32123 children: [indicators.y.barElement]
32124 }]
32125 },
32126 {
32127 children: [{
32128 children: [indicators.x.barElement]
32129 }, {}]
32130 }
32131 ]
32132 }]
32133 });
32134 },
32135
32136 updateScroller: function(scroller) {
32137 scroller.on({
32138 scope: this,
32139 scrollstart: 'onScrollStart',
32140 scroll: 'onScroll',
32141 scrollend: 'onScrollEnd',
32142 refresh: 'refreshIndicators'
32143 });
32144 },
32145
32146 isAxisEnabled: function(axis) {
32147 return this.getScroller().isAxisEnabled(axis) && this.useIndicators[axis];
32148 },
32149
32150 applyElement: function(element) {
32151 if (element) {
32152 return Ext.get(element);
32153 }
32154 },
32155
32156 updateElement: function(element) {
32157 var scroller = this.getScroller(),
32158 scrollerElement;
32159
32160
32161 scrollerElement = element.getFirstChild().getFirstChild();
32162 if (this.FixedHBoxStretching) {
32163 scrollerElement = scrollerElement.getFirstChild();
32164 }
32165
32166 element.addCls(this.getCls());
32167 element.insertFirst(this.indicatorsGrid);
32168
32169 scroller.setElement(scrollerElement);
32170
32171 this.refreshIndicators();
32172
32173 return this;
32174 },
32175
32176 showIndicators: function() {
32177 var indicators = this.getIndicators();
32178
32179 if (this.hasOwnProperty('indicatorsHidingTimer')) {
32180 clearTimeout(this.indicatorsHidingTimer);
32181 delete this.indicatorsHidingTimer;
32182 }
32183
32184 if (this.isAxisEnabled('x')) {
32185 indicators.x.show();
32186 }
32187
32188 if (this.isAxisEnabled('y')) {
32189 indicators.y.show();
32190 }
32191 },
32192
32193 hideIndicators: function() {
32194 var delay = this.getIndicatorsHidingDelay();
32195
32196 if (delay > 0) {
32197 this.indicatorsHidingTimer = setTimeout(this.doHideIndicators, delay);
32198 }
32199 else {
32200 this.doHideIndicators();
32201 }
32202 },
32203
32204 doHideIndicators: function() {
32205 var indicators = this.getIndicators();
32206
32207 if (this.isAxisEnabled('x')) {
32208 indicators.x.hide();
32209 }
32210
32211 if (this.isAxisEnabled('y')) {
32212 indicators.y.hide();
32213 }
32214 },
32215
32216 onScrollStart: function() {
32217 this.onScroll.apply(this, arguments);
32218 this.showIndicators();
32219 },
32220
32221 onScrollEnd: function() {
32222 this.hideIndicators();
32223 },
32224
32225 onScroll: function(scroller, x, y) {
32226 this.setIndicatorValue('x', x);
32227 this.setIndicatorValue('y', y);
32228
32229 //<debug>
32230 if (this.isBenchmarking) {
32231 this.framesCount++;
32232 }
32233 //</debug>
32234 },
32235
32236 //<debug>
32237 isBenchmarking: false,
32238
32239 framesCount: 0,
32240
32241 getCurrentFps: function() {
32242 var now = Date.now(),
32243 fps;
32244
32245 if (!this.isBenchmarking) {
32246 this.isBenchmarking = true;
32247 fps = 0;
32248 }
32249 else {
32250 fps = Math.round(this.framesCount * 1000 / (now - this.framesCountStartTime));
32251 }
32252
32253 this.framesCountStartTime = now;
32254 this.framesCount = 0;
32255
32256 return fps;
32257 },
32258 //</debug>
32259
32260 setIndicatorValue: function(axis, scrollerPosition) {
32261 if (!this.isAxisEnabled(axis)) {
32262 return this;
32263 }
32264
32265 var scroller = this.getScroller(),
32266 scrollerMaxPosition = scroller.getMaxPosition()[axis],
32267 scrollerContainerSize = scroller.getContainerSize()[axis],
32268 value;
32269
32270 if (scrollerMaxPosition === 0) {
32271 value = scrollerPosition / scrollerContainerSize;
32272
32273 if (scrollerPosition >= 0) {
32274 value += 1;
32275 }
32276 }
32277 else {
32278 if (scrollerPosition > scrollerMaxPosition) {
32279 value = 1 + ((scrollerPosition - scrollerMaxPosition) / scrollerContainerSize);
32280 }
32281 else if (scrollerPosition < 0) {
32282 value = scrollerPosition / scrollerContainerSize;
32283 }
32284 else {
32285 value = scrollerPosition / scrollerMaxPosition;
32286 }
32287 }
32288
32289 this.getIndicators()[axis].setValue(value);
32290 },
32291
32292 refreshIndicator: function(axis) {
32293 if (!this.isAxisEnabled(axis)) {
32294 return this;
32295 }
32296
32297 var scroller = this.getScroller(),
32298 indicator = this.getIndicators()[axis],
32299 scrollerContainerSize = scroller.getContainerSize()[axis],
32300 scrollerSize = scroller.getSize()[axis],
32301 ratio = scrollerContainerSize / scrollerSize;
32302
32303 indicator.setRatio(ratio);
32304 indicator.refresh();
32305 },
32306
32307 refresh: function() {
32308 return this.getScroller().refresh();
32309 },
32310
32311 refreshIndicators: function() {
32312 var indicators = this.getIndicators();
32313
32314 indicators.x.setActive(this.isAxisEnabled('x'));
32315 indicators.y.setActive(this.isAxisEnabled('y'));
32316
32317 this.refreshIndicator('x');
32318 this.refreshIndicator('y');
32319 },
32320
32321 destroy: function() {
32322 var element = this.getElement(),
32323 indicators = this.getIndicators();
32324
32325 Ext.destroy(this.getScroller(), this.indicatorsGrid);
32326
32327 if (this.hasOwnProperty('indicatorsHidingTimer')) {
32328 clearTimeout(this.indicatorsHidingTimer);
32329 delete this.indicatorsHidingTimer;
32330 }
32331
32332 if (element && !element.isDestroyed) {
32333 element.removeCls(this.getCls());
32334 }
32335
32336 indicators.x.destroy();
32337 indicators.y.destroy();
32338
32339 delete this.indicatorsGrid;
32340
32341 this.callParent(arguments);
32342 }
32343 });
32344
32345 /**
32346 * @private
32347 */
32348 Ext.define('Ext.behavior.Scrollable', {
32349
32350 extend: Ext.behavior.Behavior ,
32351
32352
32353
32354
32355
32356 constructor: function() {
32357 this.listeners = {
32358 painted: 'onComponentPainted',
32359 scope: this
32360 };
32361
32362 this.callParent(arguments);
32363 },
32364
32365 onComponentPainted: function() {
32366 this.scrollView.refresh();
32367 },
32368
32369 setConfig: function(config) {
32370 var scrollView = this.scrollView,
32371 component = this.component,
32372 scrollerElement, extraWrap, scroller, direction;
32373
32374 if (config) {
32375 if (!scrollView) {
32376 this.scrollView = scrollView = new Ext.scroll.View(config);
32377 scrollView.on('destroy', 'onScrollViewDestroy', this);
32378
32379 component.setUseBodyElement(true);
32380
32381 this.scrollerElement = scrollerElement = component.innerElement;
32382
32383 if (!Ext.feature.has.ProperHBoxStretching) {
32384 scroller = scrollView.getScroller();
32385 direction = (Ext.isObject(config) ? config.direction : config) || 'auto';
32386
32387 if (direction !== 'vertical') {
32388 extraWrap = scrollerElement.wrap();
32389 extraWrap.addCls(Ext.baseCSSPrefix + 'translatable-hboxfix');
32390 if (direction == 'horizontal') {
32391 extraWrap.setStyle({height: '100%'});
32392 }
32393 this.scrollContainer = extraWrap.wrap();
32394 scrollView.FixedHBoxStretching = scroller.FixedHBoxStretching = true;
32395 }
32396 else {
32397 this.scrollContainer = scrollerElement.wrap();
32398 }
32399 }
32400 else {
32401 this.scrollContainer = scrollerElement.wrap();
32402 }
32403
32404 scrollView.setElement(component.bodyElement);
32405
32406 if (component.isPainted()) {
32407 this.onComponentPainted();
32408 }
32409
32410 component.on(this.listeners);
32411 }
32412 else if (Ext.isString(config) || Ext.isObject(config)) {
32413 scrollView.setConfig(config);
32414 }
32415 }
32416 else if (scrollView) {
32417 scrollView.destroy();
32418 }
32419
32420 return this;
32421 },
32422
32423 getScrollView: function() {
32424 return this.scrollView;
32425 },
32426
32427 onScrollViewDestroy: function() {
32428 var component = this.component,
32429 scrollerElement = this.scrollerElement;
32430
32431 if (!scrollerElement.isDestroyed) {
32432 this.scrollerElement.unwrap();
32433 }
32434
32435 this.scrollContainer.destroy();
32436
32437 if (!component.isDestroyed) {
32438 component.un(this.listeners);
32439 }
32440
32441 delete this.scrollerElement;
32442 delete this.scrollView;
32443 delete this.scrollContainer;
32444 },
32445
32446 onComponentDestroy: function() {
32447 var scrollView = this.scrollView;
32448
32449 if (scrollView) {
32450 scrollView.destroy();
32451 }
32452 }
32453 });
32454
32455 /**
32456 *
32457 * @private
32458 * A utility class to disable input fields in WP7,8 because they stay still clickable even if they are under other elements.
32459 */
32460 Ext.define('Ext.util.InputBlocker', {
32461 singleton: true,
32462 blockInputs: function () {
32463 if (Ext.browser.is.ie) {
32464 Ext.select('.x-field-text .x-field-input:not(.x-item-disabled) .x-input-el, .x-field-textarea .x-field-input:not(.x-item-disabled) .x-input-el, .x-field-search .x-field-input:not(.x-item-disabled) .x-input-el').each(function (item) {
32465 if (item.dom.offsetWidth > 0) {
32466 item.dom.setAttribute('disabled', true);
32467 item.dom.setAttribute('overlayfix', true);
32468 }
32469 });
32470 }
32471 },
32472 unblockInputs: function () {
32473 if (Ext.browser.is.ie) {
32474 Ext.select('[overlayfix]').each(function (item) {
32475 item.dom.removeAttribute('disabled');
32476 item.dom.removeAttribute('overlayfix');
32477 });
32478 }
32479 }
32480 });
32481
32482 /**
32483 * A simple class used to mask any {@link Ext.Container}.
32484 *
32485 * This should rarely be used directly, instead look at the {@link Ext.Container#masked} configuration.
32486 *
32487 * ## Example
32488 *
32489 * @example miniphone
32490 * // Create our container
32491 * var container = Ext.create('Ext.Container', {
32492 * html: 'My container!'
32493 * });
32494 *
32495 * // Add the container to the Viewport
32496 * Ext.Viewport.add(container);
32497 *
32498 * // Mask the container
32499 * container.setMasked(true);
32500 */
32501 Ext.define('Ext.Mask', {
32502 extend: Ext.Component ,
32503 xtype: 'mask',
32504
32505
32506 config: {
32507 /**
32508 * @cfg
32509 * @inheritdoc
32510 */
32511 baseCls: Ext.baseCSSPrefix + 'mask',
32512
32513 /**
32514 * @cfg {Boolean} transparent True to make this mask transparent.
32515 */
32516 transparent: false,
32517
32518 /**
32519 * @cfg
32520 * @hide
32521 */
32522 top: 0,
32523
32524 /**
32525 * @cfg
32526 * @hide
32527 */
32528 left: 0,
32529
32530 /**
32531 * @cfg
32532 * @hide
32533 */
32534 right: 0,
32535
32536 /**
32537 * @cfg
32538 * @hide
32539 */
32540 bottom: 0
32541 },
32542
32543 /**
32544 * @event tap
32545 * A tap event fired when a user taps on this mask
32546 * @param {Ext.Mask} this The mask instance
32547 * @param {Ext.EventObject} e The event object
32548 */
32549 initialize: function() {
32550 this.callSuper();
32551
32552 this.element.on('*', 'onEvent', this);
32553
32554 this.on({
32555 hide: 'onHide'
32556 });
32557 },
32558
32559 onHide: function(){
32560 Ext.util.InputBlocker.unblockInputs();
32561
32562 // Oh how I loves the Android
32563 if (Ext.browser.is.AndroidStock4 && Ext.os.version.getMinor() === 0) {
32564 var firstChild = this.element.getFirstChild();
32565 if (firstChild) {
32566 firstChild.redraw();
32567 }
32568 }
32569 },
32570
32571 onEvent: function(e) {
32572 var controller = arguments[arguments.length - 1];
32573
32574 if (controller.info.eventName === 'tap') {
32575 this.fireEvent('tap', this, e);
32576 return false;
32577 }
32578
32579 if (e && e.stopEvent) {
32580 e.stopEvent();
32581 }
32582
32583 return false;
32584 },
32585
32586 updateTransparent: function(newTransparent) {
32587 this[newTransparent ? 'addCls' : 'removeCls'](this.getBaseCls() + '-transparent');
32588 }
32589 });
32590
32591 /**
32592 * A Container has all of the abilities of {@link Ext.Component Component}, but lets you nest other Components inside
32593 * it. Applications are made up of lots of components, usually nested inside one another. Containers allow you to
32594 * render and arrange child Components inside them. Most apps have a single top-level Container called a Viewport,
32595 * which takes up the entire screen. Inside of this are child components, for example in a mail app the Viewport
32596 * Container's two children might be a message List and an email preview pane.
32597 *
32598 * Containers give the following extra functionality:
32599 *
32600 * - Adding child Components at instantiation and run time
32601 * - Removing child Components
32602 * - Specifying a [Layout](../../../core_concepts/layouts.html)
32603 *
32604 * Layouts determine how the child Components should be laid out on the screen. In our mail app example we'd use an
32605 * HBox layout so that we can pin the email list to the left hand edge of the screen and allow the preview pane to
32606 * occupy the rest. There are several layouts in Sencha Touch 2, each of which help you achieve your desired
32607 * application structure, further explained in the [Layout guide](../../../core_concepts/layouts.html).
32608 *
32609 * ## Adding Components to Containers
32610 *
32611 * As we mentioned above, Containers are special Components that can have child Components arranged by a Layout. One of
32612 * the code samples above showed how to create a Panel with 2 child Panels already defined inside it but it's easy to
32613 * do this at run time too:
32614 *
32615 * @example miniphone
32616 * //this is the Panel we'll be adding below
32617 * var aboutPanel = Ext.create('Ext.Panel', {
32618 * html: 'About this app'
32619 * });
32620 *
32621 * //this is the Panel we'll be adding to
32622 * var mainPanel = Ext.create('Ext.Panel', {
32623 * fullscreen: true,
32624 *
32625 * layout: 'hbox',
32626 * defaults: {
32627 * flex: 1
32628 * },
32629 *
32630 * items: {
32631 * html: 'First Panel',
32632 * style: 'background-color: #5E99CC;'
32633 * }
32634 * });
32635 *
32636 * //now we add the first panel inside the second
32637 * mainPanel.add(aboutPanel);
32638 *
32639 * Here we created three Panels in total. First we made the aboutPanel, which we might use to tell the user a little
32640 * about the app. Then we create one called mainPanel, which already contains a third Panel in its
32641 * {@link Ext.Container#cfg-items items} configuration, with some dummy text ("First Panel"). Finally, we add the first
32642 * panel to the second by calling the {@link Ext.Container#method-add add} method on `mainPanel`.
32643 *
32644 * In this case we gave our mainPanel another hbox layout, but we also introduced some
32645 * {@link Ext.Container#defaults defaults}. These are applied to every item in the Panel, so in this case every child
32646 * inside `mainPanel` will be given a `flex: 1` configuration. The effect of this is that when we first render the screen
32647 * only a single child is present inside `mainPanel`, so that child takes up the full width available to it. Once the
32648 * `mainPanel.add` line is called though, the `aboutPanel` is rendered inside of it and also given a `flex` of 1, which will
32649 * cause it and the first panel to both receive half the full width of the `mainPanel`.
32650 *
32651 * Likewise, it's easy to remove items from a Container:
32652 *
32653 * mainPanel.remove(aboutPanel);
32654 *
32655 * After this line is run everything is back to how it was, with the first child panel once again taking up the full
32656 * width inside `mainPanel`.
32657 *
32658 * ## Further Reading
32659 *
32660 * See the [Component & Container Guide](../../../core_concepts/components.html) for more information, and check out the
32661 * {@link Ext.Container} class docs also.
32662 */
32663 Ext.define('Ext.Container', {
32664 extend: Ext.Component ,
32665
32666 alternateClassName: 'Ext.lib.Container',
32667
32668
32669
32670
32671
32672
32673
32674
32675 xtype: 'container',
32676
32677 /**
32678 * @event add
32679 * Fires whenever item added to the Container.
32680 * @param {Ext.Container} this The Container instance.
32681 * @param {Object} item The item added to the Container.
32682 * @param {Number} index The index of the item within the Container.
32683 */
32684
32685 /**
32686 * @event remove
32687 * Fires whenever item removed from the Container.
32688 * @param {Ext.Container} this The Container instance.
32689 * @param {Object} item The item removed from the Container.
32690 * @param {Number} index The index of the item that was removed.
32691 */
32692
32693 /**
32694 * @event move
32695 * Fires whenever item moved within the Container.
32696 * @param {Ext.Container} this The Container instance.
32697 * @param {Object} item The item moved within the Container.
32698 * @param {Number} toIndex The new index of the item.
32699 * @param {Number} fromIndex The old index of the item.
32700 */
32701
32702 /**
32703 * @private
32704 * @event renderedchange
32705 * Fires whenever an item is rendered into a container or derendered
32706 * from a Container.
32707 * @param {Ext.Container} this The Container instance.
32708 * @param {Object} item The item in the Container.
32709 * @param {Boolean} rendered The current rendered status of the item.
32710 */
32711
32712 /**
32713 * @event activate
32714 * Fires whenever item within the Container is activated.
32715 * @param {Object} newActiveItem The new active item within the container.
32716 * @param {Ext.Container} this The Container instance.
32717 * @param {Object} oldActiveItem The old active item within the container.
32718 */
32719
32720 /**
32721 * @event deactivate
32722 * Fires whenever item within the Container is deactivated.
32723 * @param {Object} oldActiveItem The old active item within the container.
32724 * @param {Ext.Container} this The Container instance.
32725 * @param {Object} newActiveItem The new active item within the container.
32726 */
32727
32728 eventedConfig: {
32729 /**
32730 * @cfg {Object/String/Number} activeItem The item from the {@link #cfg-items} collection that will be active first. This is
32731 * usually only meaningful in a {@link Ext.layout.Card card layout}, where only one item can be active at a
32732 * time. If passes a string, it will be assumed to be a {@link Ext.ComponentQuery} selector.
32733 * @accessor
32734 * @evented
32735 */
32736 activeItem: 0,
32737
32738 /**
32739 * @cfg {Boolean/String/Object} scrollable
32740 * Configuration options to make this Container scrollable. Acceptable values are:
32741 *
32742 * - `'horizontal'`, `'vertical'`, `'both'` to enabling scrolling for that direction.
32743 * - `true`/`false` to explicitly enable/disable scrolling.
32744 *
32745 * Alternatively, you can give it an object which is then passed to the scroller instance:
32746 *
32747 * scrollable: {
32748 * direction: 'vertical',
32749 * directionLock: true
32750 * }
32751 *
32752 * Please look at the {@link Ext.scroll.Scroller} documentation for more example on how to use this.
32753 * @return {Ext.scroll.View} The scroll view.
32754 * @accessor
32755 * @evented
32756 */
32757 scrollable: null
32758 },
32759
32760 config: {
32761 /**
32762 * @cfg {String/Object/Boolean} cardSwitchAnimation
32763 * Animation to be used during transitions of cards.
32764 * @removed 2.0.0 Please use {@link Ext.layout.Card#animation} instead
32765 */
32766
32767 /**
32768 * @cfg {Object/String} layout Configuration for this Container's layout. Example:
32769 *
32770 * Ext.create('Ext.Container', {
32771 * layout: {
32772 * type: 'hbox',
32773 * align: 'middle'
32774 * },
32775 * items: [
32776 * {
32777 * xtype: 'panel',
32778 * flex: 1,
32779 * style: 'background-color: red;'
32780 * },
32781 * {
32782 * xtype: 'panel',
32783 * flex: 2,
32784 * style: 'background-color: green'
32785 * }
32786 * ]
32787 * });
32788 *
32789 * See the [Layouts Guide](../../../core_concepts/layouts.html) for more information.
32790 *
32791 * @accessor
32792 */
32793 layout: null,
32794
32795 /**
32796 * @cfg {Object} control Enables you to easily control Components inside this Container by listening to their
32797 * events and taking some action. For example, if we had a container with a nested Disable button, and we
32798 * wanted to hide the Container when the Disable button is tapped, we could do this:
32799 *
32800 * Ext.create('Ext.Container', {
32801 * control: {
32802 * 'button[text=Disable]': {
32803 * tap: 'hideMe'
32804 * }
32805 * },
32806 *
32807 * hideMe: function () {
32808 * this.hide();
32809 * }
32810 * });
32811 *
32812 * We used a {@link Ext.ComponentQuery} selector to listen to the {@link Ext.Button#tap tap} event on any
32813 * {@link Ext.Button button} anywhere inside the Container that has the {@link Ext.Button#text text} 'Disable'.
32814 * Whenever a Component matching that selector fires the `tap` event our `hideMe` function is called. `hideMe` is
32815 * called with scope: `this` (e.g. `this` is the Container instance).
32816 *
32817 */
32818 control: {},
32819
32820 /**
32821 * @cfg {Object} defaults A set of default configurations to apply to all child Components in this Container.
32822 * It's often useful to specify defaults when creating more than one items with similar configurations. For
32823 * example here we can specify that each child is a panel and avoid repeating the xtype declaration for each
32824 * one:
32825 *
32826 * Ext.create('Ext.Container', {
32827 * defaults: {
32828 * xtype: 'panel'
32829 * },
32830 * items: [
32831 * {
32832 * html: 'Panel 1'
32833 * },
32834 * {
32835 * html: 'Panel 2'
32836 * }
32837 * ]
32838 * });
32839 *
32840 * @accessor
32841 */
32842 defaults: null,
32843
32844 /**
32845 * @cfg {Array/Object} items The child items to add to this Container. This is usually an array of Component
32846 * configurations or instances, for example:
32847 *
32848 * Ext.create('Ext.Container', {
32849 * items: [
32850 * {
32851 * xtype: 'panel',
32852 * html: 'This is an item'
32853 * }
32854 * ]
32855 * });
32856 * @accessor
32857 */
32858 items: null,
32859
32860 /**
32861 * @cfg {Boolean} autoDestroy If `true`, child items will be destroyed as soon as they are {@link #method-remove removed}
32862 * from this container.
32863 * @accessor
32864 */
32865 autoDestroy: true,
32866
32867 /** @cfg {String} defaultType
32868 * The default {@link Ext.Component xtype} of child Components to create in this Container when a child item
32869 * is specified as a raw configuration object, rather than as an instantiated Component.
32870 * @accessor
32871 */
32872 defaultType: null,
32873
32874 //@private
32875 useBodyElement: null,
32876
32877 /**
32878 * @cfg {Boolean/Object/Ext.Mask/Ext.LoadMask} masked
32879 * A configuration to allow you to mask this container.
32880 * You can optionally pass an object block with and xtype of `loadmask`, and an optional `message` value to
32881 * display a loading mask. Please refer to the {@link Ext.LoadMask} component to see other configurations.
32882 *
32883 * masked: {
32884 * xtype: 'loadmask',
32885 * message: 'My message'
32886 * }
32887 *
32888 * Alternatively, you can just call the setter at any time with `true`/`false` to show/hide the mask:
32889 *
32890 * setMasked(true); //show the mask
32891 * setMasked(false); //hides the mask
32892 *
32893 * There are also two convenient methods, {@link #method-mask} and {@link #unmask}, to allow you to mask and unmask
32894 * this container at any time.
32895 *
32896 * Remember, the {@link Ext.Viewport} is always a container, so if you want to mask your whole application at anytime,
32897 * can call:
32898 *
32899 * Ext.Viewport.setMasked({
32900 * xtype: 'loadmask',
32901 * message: 'Hello'
32902 * });
32903 *
32904 * @accessor
32905 */
32906 masked: null,
32907
32908 /**
32909 * @cfg {Boolean} modal `true` to make this Container modal. This will create a mask underneath the Container
32910 * that covers its parent and does not allow the user to interact with any other Components until this
32911 * Container is dismissed.
32912 * @accessor
32913 */
32914 modal: null,
32915
32916 /**
32917 * @cfg {Boolean} hideOnMaskTap When using a {@link #modal} Component, setting this to `true` will hide the modal
32918 * mask and the Container when the mask is tapped on.
32919 * @accessor
32920 */
32921 hideOnMaskTap: null
32922 },
32923
32924 isContainer: true,
32925
32926 constructor: function(config) {
32927 var me = this;
32928
32929 me._items = me.items = new Ext.ItemCollection();
32930 me.innerItems = [];
32931
32932 me.onItemAdd = me.onFirstItemAdd;
32933
32934 me.callParent(arguments);
32935 },
32936
32937 getElementConfig: function() {
32938 return {
32939 reference: 'element',
32940 classList: ['x-container', 'x-unsized'],
32941 children: [{
32942 reference: 'innerElement',
32943 className: 'x-inner'
32944 }]
32945 };
32946 },
32947
32948 /**
32949 * Changes the {@link #masked} configuration when its setter is called, which will convert the value
32950 * into a proper object/instance of {@link Ext.Mask}/{@link Ext.LoadMask}. If a mask already exists,
32951 * it will use that instead.
32952 * @param {Boolean/Object/Ext.Mask/Ext.LoadMask} masked
32953 * @return {Object}
32954 */
32955 applyMasked: function(masked) {
32956 var isVisible = true,
32957 currentMask;
32958
32959 if (masked === false) {
32960 masked = true;
32961 isVisible = false;
32962 }
32963
32964 currentMask = Ext.factory(masked, Ext.Mask, this.getMasked());
32965
32966 if (currentMask) {
32967 this.add(currentMask);
32968 currentMask.setHidden(!isVisible);
32969 }
32970
32971 return currentMask;
32972 },
32973
32974 /**
32975 * Convenience method which calls {@link #setMasked} with a value of `true` (to show the mask). For additional
32976 * functionality, call the {@link #setMasked} function direction (See the {@link #masked} configuration documentation
32977 * for more information).
32978 */
32979 mask: function(mask) {
32980 this.setMasked(mask || true);
32981 },
32982
32983 /**
32984 * Convenience method which calls {@link #setMasked} with a value of false (to hide the mask). For additional
32985 * functionality, call the {@link #setMasked} function direction (See the {@link #masked} configuration documentation
32986 * for more information).
32987 */
32988 unmask: function() {
32989 this.setMasked(false);
32990 },
32991
32992
32993 setParent: function(container) {
32994 this.callSuper(arguments);
32995
32996 if (container) {
32997 var modal = this.getModal();
32998
32999 if (modal) {
33000 container.insertBefore(modal, this);
33001 modal.setZIndex(this.getZIndex() - 1);
33002 }
33003 }
33004 },
33005
33006 applyModal: function(modal, currentModal) {
33007 var isVisible = true;
33008
33009 if (modal === false) {
33010 modal = true;
33011 isVisible = false;
33012 }
33013
33014 currentModal = Ext.factory(modal, Ext.Mask, currentModal);
33015
33016 if (currentModal) {
33017 currentModal.setVisibility(isVisible);
33018 }
33019
33020 return currentModal;
33021 },
33022
33023 updateModal: function(modal) {
33024 var container = this.getParent();
33025
33026 if (container) {
33027 if (modal) {
33028 container.insertBefore(modal, this);
33029 modal.setZIndex(this.getZIndex() - 1);
33030 }
33031 else {
33032 container.remove(modal);
33033 }
33034 }
33035 },
33036
33037 updateHideOnMaskTap : function(hide) {
33038 var mask = this.getModal();
33039
33040 if (mask) {
33041 mask[hide ? 'on' : 'un'].call(mask, 'tap', 'hide', this);
33042 }
33043 },
33044
33045 updateZIndex: function(zIndex) {
33046 var modal = this.getModal();
33047
33048 this.callParent(arguments);
33049
33050 if (modal) {
33051 modal.setZIndex(zIndex - 1);
33052 }
33053 },
33054
33055 updateBaseCls: function(newBaseCls, oldBaseCls) {
33056 var me = this,
33057 ui = me.getUi();
33058
33059 if (oldBaseCls) {
33060 this.element.removeCls(oldBaseCls);
33061 this.innerElement.removeCls(newBaseCls, null, 'inner');
33062
33063 if (ui) {
33064 this.element.removeCls(this.currentUi);
33065 }
33066 }
33067
33068 if (newBaseCls) {
33069 this.element.addCls(newBaseCls);
33070 this.innerElement.addCls(newBaseCls, null, 'inner');
33071
33072 if (ui) {
33073 this.element.addCls(newBaseCls, null, ui);
33074 this.currentUi = newBaseCls + '-' + ui;
33075 }
33076 }
33077 },
33078
33079 updateUseBodyElement: function(useBodyElement) {
33080 if (useBodyElement) {
33081 this.link('bodyElement', this.innerElement.wrap({
33082 cls: 'x-body'
33083 }));
33084 }
33085 },
33086
33087 applyItems: function(items, collection) {
33088 if (items) {
33089 var me = this;
33090
33091 me.getDefaultType();
33092 me.getDefaults();
33093
33094 if (me.initialized && collection.length > 0) {
33095 me.removeAll();
33096 }
33097
33098 me.add(items);
33099
33100 //Don't need to call setActiveItem when Container is first initialized
33101 if (me.initialized) {
33102 var activeItem = me.initialConfig.activeItem || me.config.activeItem || 0;
33103
33104 me.setActiveItem(activeItem);
33105 }
33106 }
33107 },
33108
33109 /**
33110 * @private
33111 */
33112 applyControl: function(selectors) {
33113 var selector, key, listener, listeners;
33114
33115 for (selector in selectors) {
33116 listeners = selectors[selector];
33117
33118 for (key in listeners) {
33119 listener = listeners[key];
33120
33121 if (Ext.isObject(listener)) {
33122 listener.delegate = selector;
33123 }
33124 }
33125
33126 listeners.delegate = selector;
33127
33128 this.addListener(listeners);
33129 }
33130
33131 return selectors;
33132 },
33133
33134 /**
33135 * Initialize layout and event listeners the very first time an item is added
33136 * @private
33137 */
33138 onFirstItemAdd: function() {
33139 delete this.onItemAdd;
33140
33141 if (this.innerHtmlElement && !this.getHtml()) {
33142 this.innerHtmlElement.destroy();
33143 delete this.innerHtmlElement;
33144 }
33145
33146 this.on('innerstatechange', 'onItemInnerStateChange', this, {
33147 delegate: '> component'
33148 });
33149
33150 return this.onItemAdd.apply(this, arguments);
33151 },
33152
33153 //<debug error>
33154 updateLayout: function(newLayout, oldLayout) {
33155 if (oldLayout && oldLayout.isLayout) {
33156 Ext.Logger.error('Replacing a layout after one has already been initialized is not currently supported.');
33157 }
33158 },
33159 //</debug>
33160
33161 getLayout: function() {
33162 var layout = this.layout;
33163 if (!layout) {
33164 layout = this.link('_layout', this.link('layout', Ext.factory(this._layout || 'default', Ext.layout.Default, null, 'layout')));
33165 layout.setContainer(this);
33166 }
33167
33168 return layout;
33169 },
33170
33171 updateDefaultType: function(defaultType) {
33172 // Cache the direct reference to the default item class here for performance
33173 this.defaultItemClass = Ext.ClassManager.getByAlias('widget.' + defaultType);
33174
33175 //<debug error>
33176 if (!this.defaultItemClass) {
33177 Ext.Logger.error("Invalid defaultType of: '" + defaultType + "', must be a valid component xtype");
33178 }
33179 //</debug>
33180 },
33181
33182 applyDefaults: function(defaults) {
33183 if (defaults) {
33184 this.factoryItem = this.factoryItemWithDefaults;
33185 return defaults;
33186 }
33187 },
33188
33189 factoryItem: function(item) {
33190 //<debug error>
33191 if (!item) {
33192 Ext.Logger.error("Invalid item given: " + item + ", must be either the config object to factory a new item, " +
33193 "or an existing component instance");
33194 }
33195 //</debug>
33196
33197 return Ext.factory(item, this.defaultItemClass);
33198 },
33199
33200 factoryItemWithDefaults: function(item) {
33201 //<debug error>
33202 if (!item) {
33203 Ext.Logger.error("Invalid item given: " + item + ", must be either the config object to factory a new item, " +
33204 "or an existing component instance");
33205 }
33206 //</debug>
33207
33208 var me = this,
33209 defaults = me.getDefaults(),
33210 instance;
33211
33212 if (!defaults) {
33213 return Ext.factory(item, me.defaultItemClass);
33214 }
33215
33216 // Existing instance
33217 if (item.isComponent) {
33218 instance = item;
33219
33220 // Apply defaults only if this is not already an item of this container
33221 if (defaults && item.isInnerItem() && !me.has(instance)) {
33222 instance.setConfig(defaults, true);
33223 }
33224 }
33225 // Config object
33226 else {
33227 if (defaults && !item.ignoreDefaults) {
33228 // Note:
33229 // - defaults is only applied to inner items
33230 // - we merge the given config together with defaults into a new object so that the original object stays intact
33231 if (!(
33232 item.hasOwnProperty('left') &&
33233 item.hasOwnProperty('right') &&
33234 item.hasOwnProperty('top') &&
33235 item.hasOwnProperty('bottom') &&
33236 item.hasOwnProperty('docked') &&
33237 item.hasOwnProperty('centered')
33238 )) {
33239 item = Ext.mergeIf({}, item, defaults);
33240 }
33241 }
33242
33243 instance = Ext.factory(item, me.defaultItemClass);
33244 }
33245
33246 return instance;
33247 },
33248
33249 /**
33250 * Adds one or more Components to this Container. Example:
33251 *
33252 * var myPanel = Ext.create('Ext.Panel', {
33253 * html: 'This will be added to a Container'
33254 * });
33255 *
33256 * myContainer.add([myPanel]);
33257 *
33258 * @param {Object/Object[]/Ext.Component/Ext.Component[]} newItems The new items to add to the Container.
33259 * @return {Ext.Component} The last item added to the Container from the `newItems` array.
33260 */
33261 add: function(newItems) {
33262 var me = this,
33263 i, ln, item, newActiveItem;
33264
33265 if (Ext.isArray(newItems)) {
33266 for (i = 0, ln = newItems.length; i < ln; i++) {
33267 item = me.factoryItem(newItems[i]);
33268 this.doAdd(item);
33269 if (!newActiveItem && !this.getActiveItem() && this.innerItems.length > 0 && item.isInnerItem()) {
33270 newActiveItem = item;
33271 }
33272 }
33273 } else {
33274 item = me.factoryItem(newItems);
33275 this.doAdd(item);
33276 if (!newActiveItem && !this.getActiveItem() && this.innerItems.length > 0 && item.isInnerItem()) {
33277 newActiveItem = item;
33278 }
33279 }
33280
33281 if (newActiveItem) {
33282 this.setActiveItem(newActiveItem);
33283 }
33284
33285 return item;
33286 },
33287
33288 /**
33289 * @private
33290 * @param {Ext.Component} item
33291 */
33292 doAdd: function(item) {
33293 var me = this,
33294 items = me.getItems(),
33295 index;
33296
33297 if (!items.has(item)) {
33298 index = items.length;
33299 items.add(item);
33300
33301 if (item.isInnerItem()) {
33302 me.insertInner(item);
33303 }
33304
33305 item.setParent(me);
33306
33307 me.onItemAdd(item, index);
33308 }
33309 },
33310
33311 /**
33312 * Removes an item from this Container, optionally destroying it.
33313 * @param {Object} item The item to remove.
33314 * @param {Boolean} [destroy] Calls the Component's {@link Ext.Component#method-destroy destroy}
33315 * method if `true`.
33316 * @return {Ext.Component} this
33317 */
33318 remove: function(item, destroy) {
33319 var me = this,
33320 index = me.indexOf(item),
33321 innerItems = me.getInnerItems();
33322
33323 if (destroy === undefined) {
33324 destroy = me.getAutoDestroy();
33325 }
33326
33327 if (index !== -1) {
33328 if (!me.removingAll && innerItems.length > 1 && item === me.getActiveItem()) {
33329 me.on({
33330 activeitemchange: 'doRemove',
33331 scope: me,
33332 single: true,
33333 order: 'after',
33334 args: [item, index, destroy]
33335 });
33336
33337 me.doResetActiveItem(innerItems.indexOf(item));
33338 }
33339 else {
33340 me.doRemove(item, index, destroy);
33341 if (innerItems.length === 0) {
33342 me.setActiveItem(null);
33343 }
33344 }
33345 }
33346
33347 return me;
33348 },
33349
33350 doResetActiveItem: function(innerIndex) {
33351 if (innerIndex === 0) {
33352 this.setActiveItem(1);
33353 }
33354 else {
33355 this.setActiveItem(0);
33356 }
33357 },
33358
33359 doRemove: function(item, index, destroy) {
33360 var me = this;
33361
33362 me.items.remove(item);
33363
33364 if (item.isInnerItem()) {
33365 me.removeInner(item);
33366 }
33367
33368 me.onItemRemove(item, index, destroy);
33369
33370 item.setParent(null);
33371
33372 if (destroy) {
33373 item.destroy();
33374 }
33375 },
33376
33377 /**
33378 * Removes all items currently in the Container, optionally destroying them all.
33379 * @param {Boolean} destroy If `true`, {@link Ext.Component#method-destroy destroys}
33380 * each removed Component.
33381 * @param {Boolean} everything If `true`, completely remove all items including
33382 * docked / centered and floating items.
33383 * @return {Ext.Component} this
33384 */
33385 removeAll: function(destroy, everything) {
33386 var items = this.items,
33387 ln = items.length,
33388 i = 0,
33389 item;
33390
33391 if (typeof destroy != 'boolean') {
33392 destroy = this.getAutoDestroy();
33393 }
33394
33395 everything = Boolean(everything);
33396
33397 // removingAll flag is used so we don't unnecessarily change activeItem while removing all items.
33398 this.removingAll = true;
33399
33400 for (; i < ln; i++) {
33401 item = items.getAt(i);
33402
33403 if (item && (everything || item.isInnerItem())) {
33404 this.doRemove(item, i, destroy);
33405 i--;
33406 ln--;
33407 }
33408 }
33409 this.setActiveItem(null);
33410
33411 this.removingAll = false;
33412
33413 return this;
33414 },
33415
33416 /**
33417 * Returns the Component for a given index in the Container's {@link #property-items}.
33418 * @param {Number} index The index of the Component to return.
33419 * @return {Ext.Component} The item at the specified `index`, if found.
33420 */
33421 getAt: function(index) {
33422 return this.items.getAt(index);
33423 },
33424
33425 getInnerAt: function(index) {
33426 return this.innerItems[index];
33427 },
33428
33429 /**
33430 * Removes the Component at the specified index:
33431 *
33432 * myContainer.removeAt(0); // removes the first item
33433 *
33434 * @param {Number} index The index of the Component to remove.
33435 */
33436 removeAt: function(index) {
33437 var item = this.getAt(index);
33438
33439 if (item) {
33440 this.remove(item);
33441 }
33442
33443 return this;
33444 },
33445
33446 /**
33447 * Removes an inner Component at the specified index:
33448 *
33449 * myContainer.removeInnerAt(0); // removes the first item of the innerItems property
33450 *
33451 * @param {Number} index The index of the Component to remove.
33452 */
33453 removeInnerAt: function(index) {
33454 var item = this.getInnerItems()[index];
33455
33456 if (item) {
33457 this.remove(item);
33458 }
33459
33460 return this;
33461 },
33462
33463 /**
33464 * @private
33465 */
33466 has: function(item) {
33467 return this.getItems().indexOf(item) != -1;
33468 },
33469
33470 /**
33471 * @private
33472 */
33473 hasInnerItem: function(item) {
33474 return this.innerItems.indexOf(item) != -1;
33475 },
33476
33477 /**
33478 * @private
33479 */
33480 indexOf: function(item) {
33481 return this.getItems().indexOf(item);
33482 },
33483
33484 innerIndexOf: function(item) {
33485 return this.innerItems.indexOf(item);
33486 },
33487
33488 /**
33489 * @private
33490 * @param {Ext.Component} item
33491 * @param {Number} index
33492 */
33493 insertInner: function(item, index) {
33494 var items = this.getItems().items,
33495 innerItems = this.innerItems,
33496 currentInnerIndex = innerItems.indexOf(item),
33497 newInnerIndex = -1,
33498 nextSibling;
33499
33500 if (currentInnerIndex !== -1) {
33501 innerItems.splice(currentInnerIndex, 1);
33502 }
33503
33504 if (typeof index == 'number') {
33505 do {
33506 nextSibling = items[++index];
33507 } while (nextSibling && !nextSibling.isInnerItem());
33508
33509 if (nextSibling) {
33510 newInnerIndex = innerItems.indexOf(nextSibling);
33511 innerItems.splice(newInnerIndex, 0, item);
33512 }
33513 }
33514
33515 if (newInnerIndex === -1) {
33516 innerItems.push(item);
33517 newInnerIndex = innerItems.length - 1;
33518 }
33519
33520 if (currentInnerIndex !== -1) {
33521 this.onInnerItemMove(item, newInnerIndex, currentInnerIndex);
33522 }
33523
33524 return this;
33525 },
33526
33527 onInnerItemMove: Ext.emptyFn,
33528
33529 /**
33530 * @private
33531 * @param {Ext.Component} item
33532 */
33533 removeInner: function(item) {
33534 Ext.Array.remove(this.innerItems, item);
33535
33536 return this;
33537 },
33538
33539 /**
33540 * Adds a child Component at the given index. For example, here's how we can add a new item, making it the first
33541 * child Component of this Container:
33542 *
33543 * myContainer.insert(0, {xtype: 'panel', html: 'new item'});
33544 *
33545 * @param {Number} index The index to insert the Component at.
33546 * @param {Object} item The Component to insert.
33547 */
33548 insert: function(index, item) {
33549 var me = this,
33550 i;
33551
33552 //<debug error>
33553 if (typeof index != 'number') {
33554 Ext.Logger.error("Invalid index of '" + index + "', must be a valid number");
33555 }
33556 //</debug>
33557
33558 if (Ext.isArray(item)) {
33559 for (i = item.length - 1; i >= 0; i--) {
33560 me.insert(index, item[i]);
33561 }
33562
33563 return me;
33564 }
33565
33566 item = this.factoryItem(item);
33567
33568 this.doInsert(index, item);
33569
33570 return item;
33571 },
33572
33573 /**
33574 * @private
33575 * @param {Number} index
33576 * @param {Ext.Component} item
33577 */
33578 doInsert: function(index, item) {
33579 var me = this,
33580 items = me.items,
33581 itemsLength = items.length,
33582 currentIndex, isInnerItem;
33583
33584 isInnerItem = item.isInnerItem();
33585
33586 if (index > itemsLength) {
33587 index = itemsLength;
33588 }
33589
33590 if (items[index - 1] === item) {
33591 return me;
33592 }
33593
33594 currentIndex = me.indexOf(item);
33595
33596 if (currentIndex !== -1) {
33597 if (currentIndex < index) {
33598 index -= 1;
33599 }
33600
33601 items.removeAt(currentIndex);
33602 }
33603
33604 items.insert(index, item);
33605
33606 if (currentIndex === -1) {
33607 item.setParent(me);
33608 }
33609
33610 if (isInnerItem) {
33611 me.insertInner(item, index);
33612 }
33613
33614 if (currentIndex !== -1) {
33615 me.onItemMove(item, index, currentIndex);
33616 }
33617 else {
33618 me.onItemAdd(item, index);
33619 }
33620 },
33621
33622 /**
33623 * @private
33624 */
33625 insertFirst: function(item) {
33626 return this.insert(0, item);
33627 },
33628
33629 /**
33630 * @private
33631 */
33632 insertLast: function(item) {
33633 return this.insert(this.getItems().length, item);
33634 },
33635
33636 /**
33637 * @private
33638 */
33639 insertBefore: function(item, relativeToItem) {
33640 var index = this.indexOf(relativeToItem);
33641
33642 if (index !== -1) {
33643 this.insert(index, item);
33644 }
33645 return this;
33646 },
33647
33648 /**
33649 * @private
33650 */
33651 insertAfter: function(item, relativeToItem) {
33652 var index = this.indexOf(relativeToItem);
33653
33654 if (index !== -1) {
33655 this.insert(index + 1, item);
33656 }
33657 return this;
33658 },
33659
33660 /**
33661 * @private
33662 */
33663 onItemAdd: function(item, index) {
33664 this.doItemLayoutAdd(item, index);
33665
33666 if (this.initialized) {
33667 this.fireEvent('add', this, item, index);
33668 }
33669 },
33670
33671 doItemLayoutAdd: function(item, index) {
33672 var layout = this.getLayout();
33673
33674 if (this.isRendered() && item.setRendered(true)) {
33675 item.fireAction('renderedchange', [this, item, true], 'onItemAdd', layout, { args: [item, index] });
33676 }
33677 else {
33678 layout.onItemAdd(item, index);
33679 }
33680 },
33681
33682 /**
33683 * @private
33684 */
33685 onItemRemove: function(item, index, destroying) {
33686 this.doItemLayoutRemove(item, index, destroying);
33687
33688 this.fireEvent('remove', this, item, index);
33689 },
33690
33691 doItemLayoutRemove: function(item, index, destroying) {
33692 var layout = this.getLayout();
33693
33694 if (this.isRendered() && item.setRendered(false)) {
33695 item.fireAction('renderedchange', [this, item, false], 'onItemRemove', layout, { args: [item, index, destroying] });
33696 }
33697 else {
33698 layout.onItemRemove(item, index, destroying);
33699 }
33700 },
33701
33702 /**
33703 * @private
33704 */
33705 onItemMove: function(item, toIndex, fromIndex) {
33706 if (item.isDocked()) {
33707 item.setDocked(null);
33708 }
33709
33710 this.doItemLayoutMove(item, toIndex, fromIndex);
33711
33712 this.fireEvent('move', this, item, toIndex, fromIndex);
33713 },
33714
33715 doItemLayoutMove: function(item, toIndex, fromIndex) {
33716 this.getLayout().onItemMove(item, toIndex, fromIndex);
33717 },
33718
33719 onItemInnerStateChange: function(item, isInner) {
33720 var layout = this.getLayout();
33721
33722 if (isInner) {
33723 this.insertInner(item, this.items.indexOf(item));
33724 }
33725 else {
33726 this.removeInner(item);
33727 }
33728
33729 layout.onItemInnerStateChange.apply(layout, arguments);
33730 },
33731
33732 /**
33733 * Returns all inner {@link #property-items} of this container. `inner` means that the item is not `docked` or
33734 * `floating`.
33735 * @return {Array} The inner items of this container.
33736 */
33737 getInnerItems: function() {
33738 return this.innerItems;
33739 },
33740
33741 /**
33742 * Returns all the {@link Ext.Component#docked} items in this container.
33743 * @return {Array} The docked items of this container.
33744 */
33745 getDockedItems: function() {
33746 var items = this.getItems().items,
33747 dockedItems = [],
33748 ln = items.length,
33749 item, i;
33750
33751 for (i = 0; i < ln; i++) {
33752 item = items[i];
33753 if (item.isDocked()) {
33754 dockedItems.push(item);
33755 }
33756 }
33757
33758 return dockedItems;
33759 },
33760
33761 /**
33762 * @private
33763 */
33764 applyActiveItem: function(activeItem, currentActiveItem) {
33765 var innerItems = this.getInnerItems();
33766
33767 // Make sure the items are already initialized
33768 this.getItems();
33769
33770 // No items left to be active, reset back to 0 on falsy changes
33771 if (!activeItem && innerItems.length === 0) {
33772 return 0;
33773 }
33774 else if (typeof activeItem == 'number') {
33775 activeItem = Math.max(0, Math.min(activeItem, innerItems.length - 1));
33776 activeItem = innerItems[activeItem];
33777
33778 if (activeItem) {
33779 return activeItem;
33780 }
33781 else if (currentActiveItem) {
33782 return null;
33783 }
33784 }
33785 else if (activeItem) {
33786 var item;
33787
33788 //ComponentQuery selector?
33789 if (typeof activeItem == 'string') {
33790 item = this.child(activeItem);
33791
33792 activeItem = {
33793 xtype : activeItem
33794 };
33795 }
33796
33797 if (!item || !item.isComponent) {
33798 item = this.factoryItem(activeItem);
33799 }
33800 this.pendingActiveItem = item;
33801
33802 //<debug error>
33803 if (!item.isInnerItem()) {
33804 Ext.Logger.error("Setting activeItem to be a non-inner item");
33805 }
33806 //</debug>
33807
33808 if (!this.has(item)) {
33809 this.add(item);
33810 }
33811
33812 return item;
33813 }
33814 },
33815
33816 /**
33817 * Animates to the supplied `activeItem` with a specified animation. Currently this only works
33818 * with a Card layout. This passed animation will override any default animations on the
33819 * container, for a single card switch. The animation will be destroyed when complete.
33820 * @param {Object/Number} activeItem The item or item index to make active.
33821 * @param {Object/Ext.fx.layout.Card} animation Card animation configuration or instance.
33822 */
33823 animateActiveItem: function(activeItem, animation) {
33824 var layout = this.getLayout(),
33825 defaultAnimation;
33826
33827 if (this.activeItemAnimation) {
33828 this.activeItemAnimation.destroy();
33829 }
33830 this.activeItemAnimation = animation = new Ext.fx.layout.Card(animation);
33831 if (animation && layout.isCard) {
33832 animation.setLayout(layout);
33833 defaultAnimation = layout.getAnimation();
33834 if (defaultAnimation) {
33835 defaultAnimation.disable();
33836 }
33837 animation.on('animationend', function() {
33838 if (defaultAnimation) {
33839 defaultAnimation.enable();
33840 }
33841 animation.destroy();
33842 }, this);
33843 }
33844 return this.setActiveItem(activeItem);
33845 },
33846
33847 /**
33848 * @private
33849 */
33850 doSetActiveItem: function(newActiveItem, oldActiveItem) {
33851 delete this.pendingActiveItem;
33852 if (oldActiveItem) {
33853 oldActiveItem.fireEvent('deactivate', oldActiveItem, this, newActiveItem);
33854 }
33855
33856 if (newActiveItem) {
33857 newActiveItem.fireEvent('activate', newActiveItem, this, oldActiveItem);
33858 }
33859 },
33860
33861 show:function(){
33862 this.callParent(arguments);
33863
33864 var modal = this.getModal();
33865
33866 if (modal) {
33867 modal.setHidden(false);
33868 }
33869
33870 return this;
33871 },
33872
33873 hide:function(){
33874 this.callParent(arguments);
33875
33876 var modal = this.getModal();
33877
33878 if (modal) {
33879 modal.setHidden(true);
33880 }
33881
33882 return this;
33883 },
33884
33885 doSetHidden: function(hidden) {
33886 var modal = this.getModal();
33887
33888 if (modal && (modal.getHidden() !== hidden)) {
33889 modal.setHidden(hidden);
33890 }
33891
33892 this.callSuper(arguments);
33893 },
33894
33895 /**
33896 * @private
33897 */
33898 setRendered: function(rendered) {
33899 if (this.callParent(arguments)) {
33900 var items = this.items.items,
33901 i, ln;
33902
33903 for (i = 0,ln = items.length; i < ln; i++) {
33904 items[i].setRendered(rendered);
33905 }
33906
33907 return true;
33908 }
33909
33910 return false;
33911 },
33912
33913 /**
33914 * @private
33915 */
33916 getScrollableBehavior: function() {
33917 var behavior = this.scrollableBehavior;
33918
33919 if (!behavior) {
33920 behavior = this.scrollableBehavior = new Ext.behavior.Scrollable(this);
33921 }
33922
33923 return behavior;
33924 },
33925
33926 /**
33927 * @private
33928 */
33929 applyScrollable: function(config) {
33930 if (typeof config === 'boolean') {
33931 //<debug warn>
33932 if (config === false && !(this.getHeight() !== null || this.heightLayoutSized || (this.getTop() !== null && this.getBottom() !== null))) {
33933 Ext.Logger.warn("This container is set to scrollable: false but has no specified height. " +
33934 "You may need to set the container to scrollable: null or provide a height.", this);
33935 }
33936 //</debug>
33937 this.getScrollableBehavior().setConfig({disabled: !config});
33938 } else if (config && !config.isObservable) {
33939 this.getScrollableBehavior().setConfig(config);
33940 }
33941 return config;
33942 },
33943
33944 doSetScrollable: function() {
33945 // Used for plugins when they need to reinitialize scroller listeners
33946 },
33947
33948 /**
33949 * Returns an the scrollable instance for this container, which is a {@link Ext.scroll.View} class.
33950 *
33951 * Please checkout the documentation for {@link Ext.scroll.View}, {@link Ext.scroll.View#getScroller}
33952 * and {@link Ext.scroll.Scroller} for more information.
33953 * @return {Ext.scroll.View} The scroll view.
33954 */
33955 getScrollable: function() {
33956 return this.getScrollableBehavior().getScrollView();
33957 },
33958
33959 // Used by ComponentQuery to retrieve all of the items
33960 // which can potentially be considered a child of this Container.
33961 // This should be overridden by components which have child items
33962 // that are not contained in items. For example `dockedItems`, `menu`, etc
33963 // @private
33964 getRefItems: function(deep) {
33965 var items = this.getItems().items.slice(),
33966 ln = items.length,
33967 i, item;
33968
33969 if (deep) {
33970 for (i = 0; i < ln; i++) {
33971 item = items[i];
33972
33973 if (item.getRefItems) {
33974 items = items.concat(item.getRefItems(true));
33975 }
33976 }
33977 }
33978
33979 return items;
33980 },
33981
33982 /**
33983 * Examines this container's `{@link #property-items}` property
33984 * and gets a direct child component of this container.
33985 * @param {String/Number} component This parameter may be any of the following:
33986 *
33987 * - {String} : representing the `itemId`
33988 * or `{@link Ext.Component#getId id}` of the child component.
33989 * - {Number} : representing the position of the child component
33990 * within the `{@link #property-items}` property.
33991 *
33992 * For additional information see {@link Ext.util.MixedCollection#get}.
33993 * @return {Ext.Component} The component (if found).
33994 */
33995 getComponent: function(component) {
33996 if (Ext.isObject(component)) {
33997 component = component.getItemId();
33998 }
33999
34000 return this.getItems().get(component);
34001 },
34002
34003 /**
34004 * Finds a docked item of this container using a reference, `id `or an `index` of its location
34005 * in {@link #getDockedItems}.
34006 * @param {String/Number} component The `id` or `index` of the component to find.
34007 * @return {Ext.Component/Boolean} The docked component, if found.
34008 */
34009 getDockedComponent: function(component) {
34010 if (Ext.isObject(component)) {
34011 component = component.getItemId();
34012 }
34013
34014 var dockedItems = this.getDockedItems(),
34015 ln = dockedItems.length,
34016 item, i;
34017
34018 if (Ext.isNumber(component)) {
34019 return dockedItems[component];
34020 }
34021
34022 for (i = 0; i < ln; i++) {
34023 item = dockedItems[i];
34024 if (item.id == component) {
34025 return item;
34026 }
34027 }
34028
34029 return false;
34030 },
34031
34032 /**
34033 * Retrieves all descendant components which match the passed selector.
34034 * Executes an Ext.ComponentQuery.query using this container as its root.
34035 * @param {String} selector Selector complying to an Ext.ComponentQuery selector.
34036 * @return {Array} Ext.Component's which matched the selector.
34037 */
34038 query: function(selector) {
34039 return Ext.ComponentQuery.query(selector, this);
34040 },
34041
34042 /**
34043 * Retrieves the first direct child of this container which matches the passed selector.
34044 * The passed in selector must comply with an {@link Ext.ComponentQuery} selector.
34045 * @param {String} selector An {@link Ext.ComponentQuery} selector.
34046 * @return {Ext.Component}
34047 */
34048 child: function(selector) {
34049 return this.query('> ' + selector)[0] || null;
34050 },
34051
34052 /**
34053 * Retrieves the first descendant of this container which matches the passed selector.
34054 * The passed in selector must comply with an {@link Ext.ComponentQuery} selector.
34055 * @param {String} selector An {@link Ext.ComponentQuery} selector.
34056 * @return {Ext.Component}
34057 */
34058 down: function(selector) {
34059 return this.query(selector)[0] || null;
34060 },
34061
34062
34063
34064 destroy: function() {
34065 var me = this,
34066 modal = me.getModal();
34067
34068 if (modal) {
34069 modal.destroy();
34070 }
34071
34072 me.removeAll(true, true);
34073 me.unlink('_scrollable');
34074 Ext.destroy(me.items);
34075
34076 me.callSuper();
34077 }
34078
34079 }, function() {
34080 this.addMember('defaultItemClass', this);
34081
34082 });
34083
34084
34085
34086 /**
34087 * Represents a 2D point with x and y properties, useful for comparison and instantiation
34088 * from an event:
34089 *
34090 * var point = Ext.util.Point.fromEvent(e);
34091 */
34092 Ext.define('Ext.util.Point', {
34093
34094 radianToDegreeConstant: 180 / Math.PI,
34095
34096 statics: {
34097 /**
34098 * Returns a new instance of {@link Ext.util.Point} based on the `pageX` / `pageY` values of the given event.
34099 * @static
34100 * @param {Event} e The event.
34101 * @return {Ext.util.Point}
34102 */
34103 fromEvent: function(e) {
34104 var changedTouches = e.changedTouches,
34105 touch = (changedTouches && changedTouches.length > 0) ? changedTouches[0] : e;
34106
34107 return this.fromTouch(touch);
34108 },
34109
34110 /**
34111 * Returns a new instance of {@link Ext.util.Point} based on the `pageX` / `pageY` values of the given touch.
34112 * @static
34113 * @param {Event} touch
34114 * @return {Ext.util.Point}
34115 */
34116 fromTouch: function(touch) {
34117 return new this(touch.pageX, touch.pageY);
34118 },
34119
34120 /**
34121 * Returns a new point from an object that has `x` and `y` properties, if that object is not an instance
34122 * of {@link Ext.util.Point}. Otherwise, returns the given point itself.
34123 * @param {Object} object
34124 * @return {Ext.util.Point}
34125 */
34126 from: function(object) {
34127 if (!object) {
34128 return new this(0, 0);
34129 }
34130
34131 if (!(object instanceof this)) {
34132 return new this(object.x, object.y);
34133 }
34134
34135 return object;
34136 }
34137 },
34138
34139 /**
34140 * Creates point on 2D plane.
34141 * @param {Number} [x=0] X coordinate.
34142 * @param {Number} [y=0] Y coordinate.
34143 */
34144 constructor: function(x, y) {
34145 if (typeof x == 'undefined') {
34146 x = 0;
34147 }
34148
34149 if (typeof y == 'undefined') {
34150 y = 0;
34151 }
34152
34153 this.x = x;
34154 this.y = y;
34155
34156 return this;
34157 },
34158
34159 /**
34160 * Copy a new instance of this point.
34161 * @return {Ext.util.Point} The new point.
34162 */
34163 clone: function() {
34164 return new this.self(this.x, this.y);
34165 },
34166
34167 /**
34168 * Clones this Point.
34169 * @deprecated 2.0.0 Please use {@link #clone} instead.
34170 * @return {Ext.util.Point} The new point.
34171 */
34172 copy: function() {
34173 return this.clone.apply(this, arguments);
34174 },
34175
34176 /**
34177 * Copy the `x` and `y` values of another point / object to this point itself.
34178 * @param {Ext.util.Point/Object} point.
34179 * @return {Ext.util.Point} This point.
34180 */
34181 copyFrom: function(point) {
34182 this.x = point.x;
34183 this.y = point.y;
34184
34185 return this;
34186 },
34187
34188 /**
34189 * Returns a human-eye-friendly string that represents this point,
34190 * useful for debugging.
34191 * @return {String} For example `Point[12,8]`.
34192 */
34193 toString: function() {
34194 return "Point[" + this.x + "," + this.y + "]";
34195 },
34196
34197 /**
34198 * Compare this point and another point.
34199 * @param {Ext.util.Point/Object} point The point to compare with, either an instance
34200 * of {@link Ext.util.Point} or an object with `x` and `y` properties.
34201 * @return {Boolean} Returns whether they are equivalent.
34202 */
34203 equals: function(point) {
34204 return (this.x === point.x && this.y === point.y);
34205 },
34206
34207 /**
34208 * Whether the given point is not away from this point within the given threshold amount.
34209 * @param {Ext.util.Point/Object} point The point to check with, either an instance
34210 * of {@link Ext.util.Point} or an object with `x` and `y` properties.
34211 * @param {Object/Number} threshold Can be either an object with `x` and `y` properties or a number.
34212 * @return {Boolean}
34213 */
34214 isCloseTo: function(point, threshold) {
34215 if (typeof threshold == 'number') {
34216 threshold = {x: threshold};
34217 threshold.y = threshold.x;
34218 }
34219
34220 var x = point.x,
34221 y = point.y,
34222 thresholdX = threshold.x,
34223 thresholdY = threshold.y;
34224
34225 return (this.x <= x + thresholdX && this.x >= x - thresholdX &&
34226 this.y <= y + thresholdY && this.y >= y - thresholdY);
34227 },
34228
34229 /**
34230 * Returns `true` if this point is close to another one.
34231 * @deprecated 2.0.0 Please use {@link #isCloseTo} instead.
34232 * @return {Boolean}
34233 */
34234 isWithin: function() {
34235 return this.isCloseTo.apply(this, arguments);
34236 },
34237
34238 /**
34239 * Translate this point by the given amounts.
34240 * @param {Number} x Amount to translate in the x-axis.
34241 * @param {Number} y Amount to translate in the y-axis.
34242 * @return {Boolean}
34243 */
34244 translate: function(x, y) {
34245 this.x += x;
34246 this.y += y;
34247
34248 return this;
34249 },
34250
34251 /**
34252 * Compare this point with another point when the `x` and `y` values of both points are rounded. For example:
34253 * [100.3,199.8] will equals to [100, 200].
34254 * @param {Ext.util.Point/Object} point The point to compare with, either an instance
34255 * of Ext.util.Point or an object with `x` and `y` properties.
34256 * @return {Boolean}
34257 */
34258 roundedEquals: function(point) {
34259 if (typeof point != 'object') {
34260 point = { x: 0, y: 0};
34261 }
34262
34263 return (Math.round(this.x) === Math.round(point.x) &&
34264 Math.round(this.y) === Math.round(point.y));
34265 },
34266
34267 getDistanceTo: function(point) {
34268 if (typeof point != 'object') {
34269 point = { x: 0, y: 0};
34270 }
34271
34272 var deltaX = this.x - point.x,
34273 deltaY = this.y - point.y;
34274
34275 return Math.sqrt(deltaX * deltaX + deltaY * deltaY);
34276 },
34277
34278 getAngleTo: function(point) {
34279 if (typeof point != 'object') {
34280 point = { x: 0, y: 0};
34281 }
34282
34283 var deltaX = this.x - point.x,
34284 deltaY = this.y - point.y;
34285
34286 return Math.atan2(deltaY, deltaX) * this.radianToDegreeConstant;
34287 }
34288 });
34289
34290 /**
34291 * @class Ext.util.LineSegment
34292 *
34293 * Utility class that represents a line segment, constructed by two {@link Ext.util.Point}
34294 */
34295 Ext.define('Ext.util.LineSegment', {
34296
34297
34298 /**
34299 * Creates new LineSegment out of two points.
34300 * @param {Ext.util.Point} point1
34301 * @param {Ext.util.Point} point2
34302 */
34303 constructor: function(point1, point2) {
34304 var Point = Ext.util.Point;
34305
34306 this.point1 = Point.from(point1);
34307 this.point2 = Point.from(point2);
34308 },
34309
34310 /**
34311 * Returns the point where two lines intersect.
34312 * @param {Ext.util.LineSegment} lineSegment The line to intersect with.
34313 * @return {Ext.util.Point}
34314 */
34315 intersects: function(lineSegment) {
34316 var point1 = this.point1,
34317 point2 = this.point2,
34318 point3 = lineSegment.point1,
34319 point4 = lineSegment.point2,
34320 x1 = point1.x,
34321 x2 = point2.x,
34322 x3 = point3.x,
34323 x4 = point4.x,
34324 y1 = point1.y,
34325 y2 = point2.y,
34326 y3 = point3.y,
34327 y4 = point4.y,
34328 d = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4),
34329 xi, yi;
34330
34331 if (d == 0) {
34332 return null;
34333 }
34334
34335 xi = ((x3 - x4) * (x1 * y2 - y1 * x2) - (x1 - x2) * (x3 * y4 - y3 * x4)) / d;
34336 yi = ((y3 - y4) * (x1 * y2 - y1 * x2) - (y1 - y2) * (x3 * y4 - y3 * x4)) / d;
34337
34338 if (xi < Math.min(x1, x2) || xi > Math.max(x1, x2)
34339 || xi < Math.min(x3, x4) || xi > Math.max(x3, x4)
34340 || yi < Math.min(y1, y2) || yi > Math.max(y1, y2)
34341 || yi < Math.min(y3, y4) || yi > Math.max(y3, y4)) {
34342 return null;
34343 }
34344
34345 return new Ext.util.Point(xi, yi);
34346 },
34347
34348 getLength: function() {
34349 return Math.abs(this.point1.getDistanceTo(this.point2));
34350 },
34351
34352 getAngleToX: function() {
34353 var point1 = this.point1,
34354 point2 = this.point2,
34355 deltaY = point2.y - point1.y,
34356 deltaX = point2.x - point1.x;
34357
34358 return Math.atan2(deltaY, deltaX);
34359 },
34360
34361 getInBetweenPoint: function(distance) {
34362 var point1 = this.point1,
34363 angle = this.getAngleToX(),
34364 x = point1.x + Math.cos(angle) * distance,
34365 y = point1.y + Math.sin(angle) * distance;
34366
34367 return new Ext.util.Point(x, y);
34368 },
34369
34370 /**
34371 * Returns string representation of the line. Useful for debugging.
34372 * @return {String} For example `Point[12,8] Point[0,0]`
34373 */
34374 toString: function() {
34375 return this.point1.toString() + " " + this.point2.toString();
34376 }
34377 });
34378
34379 /**
34380 * Panels are most useful as Overlays - containers that float over your application. They contain extra styling such
34381 * that when you {@link #showBy} another component, the container will appear in a rounded black box with a 'tip'
34382 * pointing to a reference component.
34383 *
34384 * If you don't need this extra functionality, you should use {@link Ext.Container} instead. See the
34385 * [Overlays example](#!/example/overlays) for more use cases.
34386 *
34387 * @example miniphone preview
34388 *
34389 * var button = Ext.create('Ext.Button', {
34390 * text: 'Button',
34391 * id: 'rightButton'
34392 * });
34393 *
34394 * Ext.create('Ext.Container', {
34395 * fullscreen: true,
34396 * items: [
34397 * {
34398 * docked: 'top',
34399 * xtype: 'titlebar',
34400 * items: [
34401 * button
34402 * ]
34403 * }
34404 * ]
34405 * });
34406 *
34407 * Ext.create('Ext.Panel', {
34408 * html: 'Floating Panel',
34409 * left: 0,
34410 * padding: 10
34411 * }).showBy(button);
34412 *
34413 * For more information, see our [Floating Components Guide](../../../components/floating_components.html).
34414 */
34415 Ext.define('Ext.Panel', {
34416 extend: Ext.Container ,
34417
34418
34419 alternateClassName: 'Ext.lib.Panel',
34420
34421 xtype: 'panel',
34422
34423 isPanel: true,
34424
34425 config: {
34426 baseCls: Ext.baseCSSPrefix + 'panel',
34427
34428 /**
34429 * @cfg {Number/Boolean/String} bodyPadding
34430 * A shortcut for setting a padding style on the body element. The value can either be
34431 * a number to be applied to all sides, or a normal CSS string describing padding.
34432 * @deprecated 2.0.0
34433 */
34434 bodyPadding: null,
34435
34436 /**
34437 * @cfg {Number/Boolean/String} bodyMargin
34438 * A shortcut for setting a margin style on the body element. The value can either be
34439 * a number to be applied to all sides, or a normal CSS string describing margins.
34440 * @deprecated 2.0.0
34441 */
34442 bodyMargin: null,
34443
34444 /**
34445 * @cfg {Number/Boolean/String} bodyBorder
34446 * A shortcut for setting a border style on the body element. The value can either be
34447 * a number to be applied to all sides, or a normal CSS string describing borders.
34448 * @deprecated 2.0.0
34449 */
34450 bodyBorder: null
34451 },
34452
34453 getElementConfig: function() {
34454 return {
34455 reference: 'element',
34456 classList: ['x-container', 'x-unsized'],
34457 children: [
34458 {
34459 reference: 'innerElement',
34460 className: 'x-inner'
34461 },
34462 {
34463 reference: 'tipElement',
34464 className: 'x-anchor',
34465 hidden: true
34466 }
34467 ]
34468 };
34469 },
34470
34471 applyBodyPadding: function(bodyPadding) {
34472 if (bodyPadding === true) {
34473 bodyPadding = 5;
34474 }
34475
34476 if (bodyPadding) {
34477 bodyPadding = Ext.dom.Element.unitizeBox(bodyPadding);
34478 }
34479
34480 return bodyPadding;
34481 },
34482
34483 updateBodyPadding: function(newBodyPadding) {
34484 this.element.setStyle('padding', newBodyPadding);
34485 },
34486
34487 applyBodyMargin: function(bodyMargin) {
34488 if (bodyMargin === true) {
34489 bodyMargin = 5;
34490 }
34491
34492 if (bodyMargin) {
34493 bodyMargin = Ext.dom.Element.unitizeBox(bodyMargin);
34494 }
34495
34496 return bodyMargin;
34497 },
34498
34499 updateBodyMargin: function(newBodyMargin) {
34500 this.element.setStyle('margin', newBodyMargin);
34501 },
34502
34503 applyBodyBorder: function(bodyBorder) {
34504 if (bodyBorder === true) {
34505 bodyBorder = 1;
34506 }
34507
34508 if (bodyBorder) {
34509 bodyBorder = Ext.dom.Element.unitizeBox(bodyBorder);
34510 }
34511
34512 return bodyBorder;
34513 },
34514
34515 updateBodyBorder: function(newBodyBorder) {
34516 this.element.setStyle('border-width', newBodyBorder);
34517 },
34518
34519 alignTo: function(component, alignment) {
34520 var alignmentInfo = this.getAlignmentInfo(component, alignment);
34521 if(alignmentInfo.isAligned) return;
34522 var tipElement = this.tipElement;
34523
34524 tipElement.hide();
34525
34526 if (this.currentTipPosition) {
34527 tipElement.removeCls('x-anchor-' + this.currentTipPosition);
34528 }
34529
34530 this.callParent(arguments);
34531
34532 var LineSegment = Ext.util.LineSegment,
34533 alignToElement = component.isComponent ? component.renderElement : component,
34534 element = this.renderElement,
34535 alignToBox = alignToElement.getPageBox(),
34536 box = element.getPageBox(),
34537 left = box.left,
34538 top = box.top,
34539 right = box.right,
34540 bottom = box.bottom,
34541 centerX = left + (box.width / 2),
34542 centerY = top + (box.height / 2),
34543 leftTopPoint = { x: left, y: top },
34544 rightTopPoint = { x: right, y: top },
34545 leftBottomPoint = { x: left, y: bottom },
34546 rightBottomPoint = { x: right, y: bottom },
34547 boxCenterPoint = { x: centerX, y: centerY },
34548 alignToCenterX = alignToBox.left + (alignToBox.width / 2),
34549 alignToCenterY = alignToBox.top + (alignToBox.height / 2),
34550 alignToBoxCenterPoint = { x: alignToCenterX, y: alignToCenterY },
34551 centerLineSegment = new LineSegment(boxCenterPoint, alignToBoxCenterPoint),
34552 offsetLeft = 0,
34553 offsetTop = 0,
34554 tipSize, tipWidth, tipHeight, tipPosition, tipX, tipY;
34555
34556 tipElement.setVisibility(false);
34557 tipElement.show();
34558 tipSize = tipElement.getSize();
34559 tipWidth = tipSize.width;
34560 tipHeight = tipSize.height;
34561
34562 if (centerLineSegment.intersects(new LineSegment(leftTopPoint, rightTopPoint))) {
34563 tipX = Math.min(Math.max(alignToCenterX, left + tipWidth), right - (tipWidth));
34564 tipY = top;
34565 offsetTop = tipHeight + 10;
34566 tipPosition = 'top';
34567 }
34568 else if (centerLineSegment.intersects(new LineSegment(leftTopPoint, leftBottomPoint))) {
34569 tipX = left;
34570 tipY = Math.min(Math.max(alignToCenterY + (tipWidth / 2), tipWidth * 1.6), bottom - (tipWidth / 2.2));
34571 offsetLeft = tipHeight + 10;
34572 tipPosition = 'left';
34573 }
34574 else if (centerLineSegment.intersects(new LineSegment(leftBottomPoint, rightBottomPoint))) {
34575 tipX = Math.min(Math.max(alignToCenterX, left + tipWidth), right - tipWidth);
34576 tipY = bottom;
34577 offsetTop = -tipHeight - 10;
34578 tipPosition = 'bottom';
34579 }
34580 else if (centerLineSegment.intersects(new LineSegment(rightTopPoint, rightBottomPoint))) {
34581 tipX = right;
34582 tipY = Math.max(Math.min(alignToCenterY - tipHeight, bottom - tipWidth * 1.3), tipWidth / 2);
34583 offsetLeft = -tipHeight - 10;
34584 tipPosition = 'right';
34585 }
34586
34587 if (tipX || tipY) {
34588 this.currentTipPosition = tipPosition;
34589 tipElement.addCls('x-anchor-' + tipPosition);
34590 tipElement.setLeft(tipX - left);
34591 tipElement.setTop(tipY - top);
34592 tipElement.setVisibility(true);
34593
34594 this.setLeft(this.getLeft() + offsetLeft);
34595 this.setTop(this.getTop() + offsetTop);
34596 }
34597 }
34598 });
34599
34600 /**
34601 * A simple class to display a button in Sencha Touch.
34602 *
34603 * There are various different styles of Button you can create by using the {@link #icon},
34604 * {@link #iconCls}, {@link #iconAlign}, {@link #ui}, and {@link #text}
34605 * configurations.
34606 *
34607 * ## Simple Button
34608 *
34609 * Here is a Button in it's simplest form:
34610 *
34611 * @example miniphone
34612 * var button = Ext.create('Ext.Button', {
34613 * text: 'Button'
34614 * });
34615 * Ext.Viewport.add({ xtype: 'container', padding: 10, items: [button] });
34616 *
34617 * ## Icons
34618 *
34619 * You can also create a Button with just an icon using the {@link #iconCls} configuration:
34620 *
34621 * @example miniphone
34622 * var button = Ext.create('Ext.Button', {
34623 * iconCls: 'refresh'
34624 * });
34625 * Ext.Viewport.add({ xtype: 'container', padding: 10, items: [button] });
34626 *
34627 * Sencha provides the "Font" and "PNG" icons packs from http://wwww.pictos.cc.
34628 * Use icons with the {@link Global_CSS#icon icon} mixin in your Sass.
34629 *
34630 * ## Badges
34631 *
34632 * Buttons can also have a badge on them, by using the {@link #badgeText} configuration:
34633 *
34634 * @example
34635 * Ext.create('Ext.Container', {
34636 * fullscreen: true,
34637 * padding: 10,
34638 * items: {
34639 * xtype: 'button',
34640 * text: 'My Button',
34641 * badgeText: '2'
34642 * }
34643 * });
34644 *
34645 * ## UI
34646 *
34647 * Buttons also come with a range of different default UIs. Here are the included UIs
34648 * available (if {@link #$include-button-uis $include-button-uis} is set to `true`):
34649 *
34650 * - **normal** - a basic gray button
34651 * - **back** - a back button
34652 * - **forward** - a forward button
34653 * - **round** - a round button
34654 * - **action** - shaded using the {@link Global_CSS#$active-color $active-color} (dark blue by default)
34655 * - **decline** - shaded using the {@link Global_CSS#$alert-color $alert-color} (red by default)
34656 * - **confirm** - shaded using the {@link Global_CSS#$confirm-color $confirm-color} (green by default)
34657 *
34658 * You can also append `-round` to each of the last three UI's to give it a round shape:
34659 *
34660 * - **action-round**
34661 * - **decline-round**
34662 * - **confirm-round**
34663 *
34664 * And setting them is very simple:
34665 *
34666 * var uiButton = Ext.create('Ext.Button', {
34667 * text: 'My Button',
34668 * ui: 'action'
34669 * });
34670 *
34671 * And how they look:
34672 *
34673 * @example miniphone preview
34674 * Ext.create('Ext.Container', {
34675 * fullscreen: true,
34676 * padding: 4,
34677 * defaults: {
34678 * xtype: 'button',
34679 * margin: 5
34680 * },
34681 * layout: {
34682 * type: 'vbox',
34683 * align: 'center'
34684 * },
34685 * items: [
34686 * { ui: 'normal', text: 'normal' },
34687 * { ui: 'round', text: 'round' },
34688 * { ui: 'action', text: 'action' },
34689 * { ui: 'decline', text: 'decline' },
34690 * { ui: 'confirm', text: 'confirm' }
34691 * ]
34692 * });
34693 *
34694 * Note that the default {@link #ui} is **normal**.
34695 *
34696 * You can also use the {@link #sencha-button-ui sencha-button-ui} CSS Mixin to create your own UIs.
34697 *
34698 * ## Example
34699 *
34700 * This example shows a bunch of icons on the screen in two toolbars. When you click on the center
34701 * button, it switches the {@link #iconCls} on every button on the page.
34702 *
34703 * @example preview
34704 * Ext.createWidget('container', {
34705 * fullscreen: true,
34706 * layout: {
34707 * type: 'vbox',
34708 * pack:'center',
34709 * align: 'center'
34710 * },
34711 * items: [
34712 * {
34713 * xtype: 'button',
34714 * text: 'Change iconCls',
34715 * handler: function() {
34716 * // classes for all the icons to loop through.
34717 * var availableIconCls = [
34718 * 'action', 'add', 'arrow_down', 'arrow_left',
34719 * 'arrow_right', 'arrow_up', 'compose', 'delete',
34720 * 'organize', 'refresh', 'reply', 'search',
34721 * 'settings', 'star', 'trash', 'maps', 'locate',
34722 * 'home'
34723 * ];
34724 * // get the text of this button,
34725 * // so we know which button we don't want to change
34726 * var text = this.getText();
34727 *
34728 * // use ComponentQuery to find all buttons on the page
34729 * // and loop through all of them
34730 * Ext.Array.forEach(Ext.ComponentQuery.query('button'), function(button) {
34731 * // if the button is the change iconCls button, continue
34732 * if (button.getText() === text) {
34733 * return;
34734 * }
34735 *
34736 * // get the index of the new available iconCls
34737 * var index = availableIconCls.indexOf(button.getIconCls()) + 1;
34738 *
34739 * // update the iconCls of the button with the next iconCls, if one exists.
34740 * // if not, use the first one
34741 * button.setIconCls(availableIconCls[(index === availableIconCls.length) ? 0 : index]);
34742 * });
34743 * }
34744 * },
34745 * {
34746 * xtype: 'toolbar',
34747 * docked: 'top',
34748 * items: [
34749 * { xtype: 'spacer' },
34750 * { iconCls: 'action' },
34751 * { iconCls: 'add' },
34752 * { iconCls: 'arrow_down' },
34753 * { iconCls: 'arrow_left' },
34754 * { iconCls: 'arrow_up' },
34755 * { iconCls: 'compose' },
34756 * { iconCls: 'delete' },
34757 * { iconCls: 'organize' },
34758 * { iconCls: 'refresh' },
34759 * { xtype: 'spacer' }
34760 * ]
34761 * },
34762 * {
34763 * xtype: 'toolbar',
34764 * docked: 'bottom',
34765 * ui: 'light',
34766 * items: [
34767 * { xtype: 'spacer' },
34768 * { iconCls: 'reply' },
34769 * { iconCls: 'search' },
34770 * { iconCls: 'settings' },
34771 * { iconCls: 'star' },
34772 * { iconCls: 'trash' },
34773 * { iconCls: 'maps' },
34774 * { iconCls: 'locate' },
34775 * { iconCls: 'home' },
34776 * { xtype: 'spacer' }
34777 * ]
34778 * }
34779 * ]
34780 * });
34781 *
34782 */
34783 Ext.define('Ext.Button', {
34784 extend: Ext.Component ,
34785
34786 xtype: 'button',
34787
34788 /**
34789 * @event tap
34790 * @preventable doTap
34791 * Fires whenever a button is tapped.
34792 * @param {Ext.Button} this The item added to the Container.
34793 * @param {Ext.EventObject} e The event object.
34794 */
34795
34796 /**
34797 * @event release
34798 * @preventable doRelease
34799 * Fires whenever the button is released.
34800 * @param {Ext.Button} this The item added to the Container.
34801 * @param {Ext.EventObject} e The event object.
34802 */
34803
34804 cachedConfig: {
34805 /**
34806 * @cfg {String} pressedCls
34807 * The CSS class to add to the Button when it is pressed.
34808 * @accessor
34809 */
34810 pressedCls: Ext.baseCSSPrefix + 'button-pressing',
34811
34812 /**
34813 * @cfg {String} badgeCls
34814 * The CSS class to add to the Button's badge, if it has one. Badges appear as small numbers, letters, or icons that sit on top of your button. For instance, a small red number indicating how many updates are available.
34815 * @accessor
34816 */
34817 badgeCls: Ext.baseCSSPrefix + 'badge',
34818
34819 /**
34820 * @cfg {String} hasBadgeCls
34821 * The CSS class to add to the Button if it has a badge (note that this goes on the
34822 * Button element itself, not on the badge element).
34823 * @private
34824 * @accessor
34825 */
34826 hasBadgeCls: Ext.baseCSSPrefix + 'hasbadge',
34827
34828 /**
34829 * @cfg {String} labelCls
34830 * The CSS class to add to the field's label element.
34831 * @accessor
34832 */
34833 labelCls: Ext.baseCSSPrefix + 'button-label',
34834
34835 /**
34836 * @cfg {String} iconCls
34837 * Optional CSS class to add to the icon element. This is useful if you want to use a CSS
34838 * background image to create your Button icon.
34839 * @accessor
34840 */
34841 iconCls: null
34842 },
34843
34844 config: {
34845 /**
34846 * @cfg {String} badgeText
34847 * Optional badge text. Badges appear as small numbers, letters, or icons that sit on top of your button. For instance, a small red number indicating how many updates are available.
34848 * @accessor
34849 */
34850 badgeText: null,
34851
34852 /**
34853 * @cfg {String} text
34854 * The Button text.
34855 * @accessor
34856 */
34857 text: null,
34858
34859 /**
34860 * @cfg {String} icon
34861 * Url to the icon image to use if you want an icon to appear on your button.
34862 * @accessor
34863 */
34864 icon: false,
34865
34866 /**
34867 * @cfg {String} iconAlign
34868 * The position within the Button to render the icon Options are: `top`, `right`, `bottom`, `left` and `center` (when you have
34869 * no {@link #text} set).
34870 * @accessor
34871 */
34872 iconAlign: 'left',
34873
34874 /**
34875 * @cfg {Number/Boolean} pressedDelay
34876 * The amount of delay between the `tapstart` and the moment we add the `pressedCls` (in milliseconds).
34877 * Settings this to `true` defaults to 100ms.
34878 */
34879 pressedDelay: 0,
34880
34881 /**
34882 * @cfg {Function} handler
34883 * The handler function to run when the Button is tapped on.
34884 * @accessor
34885 */
34886 handler: null,
34887
34888 /**
34889 * @cfg {Object} scope
34890 * The scope to fire the configured {@link #handler} in.
34891 * @accessor
34892 */
34893 scope: null,
34894
34895 /**
34896 * @cfg {String} autoEvent
34897 * Optional event name that will be fired instead of `tap` when the Button is tapped on.
34898 * @accessor
34899 */
34900 autoEvent: null,
34901
34902 /**
34903 * @cfg {String} ui
34904 * The ui style to render this button with. The valid default options are:
34905 *
34906 * - `'normal'` - a basic gray button (default).
34907 * - `'back'` - a back button.
34908 * - `'forward'` - a forward button.
34909 * - `'round'` - a round button.
34910 * - `'plain'`
34911 * - `'action'` - shaded using the {@link Global_CSS#$active-color $active-color} (dark blue by default).
34912 * - `'decline'` - shaded using the {@link Global_CSS#$alert-color $alert-color} (red by default).
34913 * - `'confirm'` - shaded using the {@link Global_CSS#$confirm-color $confirm-color} (green by default).
34914 *
34915 * You can also append `-round` to each of the last three UI's to give it a round shape:
34916 *
34917 * - **action-round**
34918 * - **decline-round**
34919 * - **confirm-round**
34920 *
34921 * @accessor
34922 */
34923 ui: 'normal',
34924
34925 /**
34926 * @cfg {String} html The HTML to put in this button.
34927 *
34928 * If you want to just add text, please use the {@link #text} configuration.
34929 */
34930
34931 /**
34932 * @cfg
34933 * @inheritdoc
34934 */
34935 baseCls: Ext.baseCSSPrefix + 'button'
34936 },
34937
34938 template: [
34939 {
34940 tag: 'span',
34941 reference: 'badgeElement',
34942 hidden: true
34943 },
34944 {
34945 tag: 'span',
34946 className: Ext.baseCSSPrefix + 'button-icon',
34947 reference: 'iconElement'
34948 },
34949 {
34950 tag: 'span',
34951 reference: 'textElement',
34952 hidden: true
34953 }
34954 ],
34955
34956 initialize: function() {
34957 this.callParent();
34958
34959 this.element.on({
34960 scope : this,
34961 tap : 'onTap',
34962 touchstart : 'onPress',
34963 touchend : 'onRelease'
34964 });
34965 },
34966
34967 /**
34968 * @private
34969 */
34970 updateBadgeText: function(badgeText) {
34971 var element = this.element,
34972 badgeElement = this.badgeElement;
34973
34974 if (badgeText) {
34975 badgeElement.show();
34976 badgeElement.setText(badgeText);
34977 }
34978 else {
34979 badgeElement.hide();
34980 }
34981
34982 element[(badgeText) ? 'addCls' : 'removeCls'](this.getHasBadgeCls());
34983 },
34984
34985 /**
34986 * @private
34987 */
34988 updateText: function(text) {
34989 var textElement = this.textElement;
34990
34991 if (textElement) {
34992 if (text) {
34993 textElement.show();
34994 textElement.setHtml(text);
34995 } else {
34996 textElement.hide();
34997 }
34998
34999 this.refreshIconAlign();
35000 }
35001 },
35002
35003 /**
35004 * @private
35005 */
35006 updateHtml: function(html) {
35007 var textElement = this.textElement;
35008
35009 if (html) {
35010 textElement.show();
35011 textElement.setHtml(html);
35012 }
35013 else {
35014 textElement.hide();
35015 }
35016 },
35017
35018 /**
35019 * @private
35020 */
35021 updateBadgeCls: function(badgeCls, oldBadgeCls) {
35022 this.badgeElement.replaceCls(oldBadgeCls, badgeCls);
35023 },
35024
35025 /**
35026 * @private
35027 */
35028 updateHasBadgeCls: function(hasBadgeCls, oldHasBadgeCls) {
35029 var element = this.element;
35030
35031 if (element.hasCls(oldHasBadgeCls)) {
35032 element.replaceCls(oldHasBadgeCls, hasBadgeCls);
35033 }
35034 },
35035
35036 /**
35037 * @private
35038 */
35039 updateLabelCls: function(labelCls, oldLabelCls) {
35040 this.textElement.replaceCls(oldLabelCls, labelCls);
35041 },
35042
35043 /**
35044 * @private
35045 */
35046 updatePressedCls: function(pressedCls, oldPressedCls) {
35047 var element = this.element;
35048
35049 if (element.hasCls(oldPressedCls)) {
35050 element.replaceCls(oldPressedCls, pressedCls);
35051 }
35052 },
35053
35054 /**
35055 * @private
35056 */
35057 updateIcon: function(icon) {
35058 var me = this,
35059 element = me.iconElement;
35060
35061 if (icon) {
35062 me.showIconElement();
35063 element.setStyle('background-image', 'url(' + icon + ')');
35064 me.refreshIconAlign();
35065 } else {
35066 element.setStyle('background-image', '');
35067 me.hideIconElement();
35068 }
35069 },
35070
35071 /**
35072 * @private
35073 */
35074 updateIconCls: function(iconCls, oldIconCls) {
35075 var me = this,
35076 element = me.iconElement;
35077
35078 if (iconCls) {
35079 me.showIconElement();
35080 element.replaceCls(oldIconCls, iconCls);
35081 me.refreshIconAlign();
35082 } else {
35083 element.removeCls(oldIconCls);
35084 me.hideIconElement();
35085 }
35086 },
35087
35088 /**
35089 * @private
35090 */
35091 updateIconAlign: function(alignment, oldAlignment) {
35092 var element = this.element,
35093 baseCls = Ext.baseCSSPrefix + 'iconalign-';
35094
35095 if (!this.getText()) {
35096 alignment = "center";
35097 }
35098
35099 element.removeCls(baseCls + "center");
35100 element.removeCls(baseCls + oldAlignment);
35101 if (this.getIcon() || this.getIconCls()) {
35102 element.addCls(baseCls + alignment);
35103 }
35104 },
35105
35106 refreshIconAlign: function() {
35107 this.updateIconAlign(this.getIconAlign());
35108 },
35109
35110 applyAutoEvent: function(autoEvent) {
35111 var me = this;
35112
35113 if (typeof autoEvent == 'string') {
35114 autoEvent = {
35115 name : autoEvent,
35116 scope: me.scope || me
35117 };
35118 }
35119
35120 return autoEvent;
35121 },
35122
35123 /**
35124 * @private
35125 */
35126 updateAutoEvent: function(autoEvent) {
35127 var name = autoEvent.name,
35128 scope = autoEvent.scope;
35129
35130 this.setHandler(function() {
35131 scope.fireEvent(name, scope, this);
35132 });
35133
35134 this.setScope(scope);
35135 },
35136
35137 /**
35138 * Used by `icon` and `iconCls` configurations to hide the icon element.
35139 * @private
35140 */
35141 hideIconElement: function() {
35142 this.iconElement.removeCls(Ext.baseCSSPrefix + 'shown');
35143 this.iconElement.addCls(Ext.baseCSSPrefix + 'hidden');
35144 },
35145
35146 /**
35147 * Used by `icon` and `iconCls` configurations to show the icon element.
35148 * @private
35149 */
35150 showIconElement: function() {
35151 this.iconElement.removeCls(Ext.baseCSSPrefix + 'hidden');
35152 this.iconElement.addCls(Ext.baseCSSPrefix + 'shown');
35153 },
35154
35155 /**
35156 * We override this to check for '{ui}-back'. This is because if you have a UI of back, you need to actually add two class names.
35157 * The ui class, and the back class:
35158 *
35159 * `ui: 'action-back'` would turn into:
35160 *
35161 * `class="x-button-action x-button-back"`
35162 *
35163 * But `ui: 'action'` would turn into:
35164 *
35165 * `class="x-button-action"`
35166 *
35167 * So we just split it up into an array and add both of them as a UI, when it has `back`.
35168 * @private
35169 */
35170 applyUi: function(config) {
35171 if (config && Ext.isString(config)) {
35172 var array = config.split('-');
35173 if (array && (array[1] == "back" || array[1] == "forward")) {
35174 return array;
35175 }
35176 }
35177
35178 return config;
35179 },
35180
35181 getUi: function() {
35182 //Now that the UI can sometimes be an array, we need to check if it an array and return the proper value.
35183 var ui = this._ui;
35184 if (Ext.isArray(ui)) {
35185 return ui.join('-');
35186 }
35187 return ui;
35188 },
35189
35190 applyPressedDelay: function(delay) {
35191 if (Ext.isNumber(delay)) {
35192 return delay;
35193 }
35194 return (delay) ? 100 : 0;
35195 },
35196
35197 // @private
35198 onPress: function() {
35199 var me = this,
35200 element = me.element,
35201 pressedDelay = me.getPressedDelay(),
35202 pressedCls = me.getPressedCls();
35203
35204 if (!me.getDisabled()) {
35205 if (pressedDelay > 0) {
35206 me.pressedTimeout = setTimeout(function() {
35207 delete me.pressedTimeout;
35208 if (element) {
35209 element.addCls(pressedCls);
35210 }
35211 }, pressedDelay);
35212 }
35213 else {
35214 element.addCls(pressedCls);
35215 }
35216 }
35217 },
35218
35219 // @private
35220 onRelease: function(e) {
35221 this.fireAction('release', [this, e], 'doRelease');
35222 },
35223
35224 // @private
35225 doRelease: function(me, e) {
35226 if (!me.getDisabled()) {
35227 if (me.hasOwnProperty('pressedTimeout')) {
35228 clearTimeout(me.pressedTimeout);
35229 delete me.pressedTimeout;
35230 }
35231 else {
35232 me.element.removeCls(me.getPressedCls());
35233 }
35234 }
35235 },
35236
35237 // @private
35238 onTap: function(e) {
35239 if (this.getDisabled()) {
35240 return false;
35241 }
35242
35243 this.fireAction('tap', [this, e], 'doTap');
35244 },
35245
35246 /**
35247 * @private
35248 */
35249 doTap: function(me, e) {
35250 var handler = me.getHandler(),
35251 scope = me.getScope() || me;
35252
35253 if (!handler) {
35254 return;
35255 }
35256
35257 if (typeof handler == 'string') {
35258 handler = scope[handler];
35259 }
35260
35261 //this is done so if you hide the button in the handler, the tap event will not fire on the new element
35262 //where the button was.
35263 if (e && e.preventDefault) {
35264 e.preventDefault();
35265 }
35266
35267 handler.apply(scope, arguments);
35268 }
35269 }, function() {
35270 });
35271
35272 /**
35273 * A general sheet class. This renderable container provides base support for orientation-aware transitions for popup or
35274 * side-anchored sliding Panels.
35275 *
35276 * In most cases, you should use {@link Ext.ActionSheet}, {@link Ext.MessageBox}, {@link Ext.picker.Picker}, or {@link Ext.picker.Date}.
35277 */
35278 Ext.define('Ext.Sheet', {
35279 extend: Ext.Panel ,
35280
35281 xtype: 'sheet',
35282
35283
35284
35285 config: {
35286 /**
35287 * @cfg
35288 * @inheritdoc
35289 */
35290 baseCls: Ext.baseCSSPrefix + 'sheet',
35291
35292 /**
35293 * @cfg
35294 * @inheritdoc
35295 */
35296 modal: true,
35297
35298 /**
35299 * @cfg {Boolean} centered
35300 * Whether or not this component is absolutely centered inside its container.
35301 * @accessor
35302 * @evented
35303 */
35304 centered: true,
35305
35306 /**
35307 * @cfg {Boolean} stretchX `true` to stretch this sheet horizontally.
35308 */
35309 stretchX: null,
35310
35311 /**
35312 * @cfg {Boolean} stretchY `true` to stretch this sheet vertically.
35313 */
35314 stretchY: null,
35315
35316 /**
35317 * @cfg {String} enter
35318 * The viewport side used as the enter point when shown. Valid values are 'top', 'bottom', 'left', and 'right'.
35319 * Applies to sliding animation effects only.
35320 */
35321 enter: 'bottom',
35322
35323 /**
35324 * @cfg {String} exit
35325 * The viewport side used as the exit point when hidden. Valid values are 'top', 'bottom', 'left', and 'right'.
35326 * Applies to sliding animation effects only.
35327 */
35328 exit: 'bottom',
35329
35330 /**
35331 * @cfg
35332 * @inheritdoc
35333 */
35334 showAnimation: !Ext.browser.is.AndroidStock2 ? {
35335 type: 'slideIn',
35336 duration: 250,
35337 easing: 'ease-out'
35338 } : null,
35339
35340 /**
35341 * @cfg
35342 * @inheritdoc
35343 */
35344 hideAnimation: !Ext.browser.is.AndroidStock2 ? {
35345 type: 'slideOut',
35346 duration: 250,
35347 easing: 'ease-in'
35348 } : null
35349 },
35350
35351 isInputRegex: /^(input|textarea|select|a)$/i,
35352
35353 beforeInitialize: function() {
35354 var me = this;
35355 // Temporary fix for a mysterious bug on iOS where double tapping on a sheet
35356 // being animated from the bottom shift the whole body up
35357 Ext.os.is.iOS && this.element.dom.addEventListener('touchstart', function(e) {
35358 if (!me.isInputRegex.test(e.target.tagName)) {
35359 e.preventDefault();
35360 }
35361 }, true);
35362 },
35363
35364 platformConfig: [{
35365 theme: ['Windows'],
35366 enter: 'top',
35367 exit: 'top'
35368 }],
35369
35370 applyHideAnimation: function(config) {
35371 var exit = this.getExit(),
35372 direction = exit;
35373
35374 if (exit === null) {
35375 return null;
35376 }
35377
35378 if (config === true) {
35379 config = {
35380 type: 'slideOut'
35381 };
35382 }
35383 if (Ext.isString(config)) {
35384 config = {
35385 type: config
35386 };
35387 }
35388 var anim = Ext.factory(config, Ext.fx.Animation);
35389
35390 if (anim) {
35391 if (exit == 'bottom') {
35392 direction = 'down';
35393 }
35394 if (exit == 'top') {
35395 direction = 'up';
35396 }
35397 anim.setDirection(direction);
35398 }
35399 return anim;
35400 },
35401
35402 applyShowAnimation: function(config) {
35403 var enter = this.getEnter(),
35404 direction = enter;
35405
35406 if (enter === null) {
35407 return null;
35408 }
35409
35410 if (config === true) {
35411 config = {
35412 type: 'slideIn'
35413 };
35414 }
35415 if (Ext.isString(config)) {
35416 config = {
35417 type: config
35418 };
35419 }
35420 var anim = Ext.factory(config, Ext.fx.Animation);
35421
35422 if (anim) {
35423 if (enter == 'bottom') {
35424 direction = 'down';
35425 }
35426 if (enter == 'top') {
35427 direction = 'up';
35428 }
35429 anim.setBefore({
35430 display: null
35431 });
35432 anim.setReverse(true);
35433 anim.setDirection(direction);
35434 }
35435 return anim;
35436 },
35437
35438 updateStretchX: function(newStretchX) {
35439 this.getLeft();
35440 this.getRight();
35441
35442 if (newStretchX) {
35443 this.setLeft(0);
35444 this.setRight(0);
35445 }
35446 },
35447
35448 updateStretchY: function(newStretchY) {
35449 this.getTop();
35450 this.getBottom();
35451
35452 if (newStretchY) {
35453 this.setTop(0);
35454 this.setBottom(0);
35455 }
35456 }
35457 });
35458
35459 /**
35460 * {@link Ext.ActionSheet ActionSheets} are used to display a list of {@link Ext.Button buttons} in a popup dialog.
35461 *
35462 * The key difference between ActionSheet and {@link Ext.Sheet} is that ActionSheets are docked at the bottom of the
35463 * screen, and the {@link #defaultType} is set to {@link Ext.Button button}.
35464 *
35465 * ## Example
35466 *
35467 * @example preview miniphone
35468 * var actionSheet = Ext.create('Ext.ActionSheet', {
35469 * items: [
35470 * {
35471 * text: 'Delete draft',
35472 * ui : 'decline'
35473 * },
35474 * {
35475 * text: 'Save draft'
35476 * },
35477 * {
35478 * text: 'Cancel',
35479 * ui : 'confirm'
35480 * }
35481 * ]
35482 * });
35483 *
35484 * Ext.Viewport.add(actionSheet);
35485 * actionSheet.show();
35486 *
35487 * As you can see from the code above, you no longer have to specify a `xtype` when creating buttons within a {@link Ext.ActionSheet ActionSheet},
35488 * because the {@link #defaultType} is set to {@link Ext.Button button}.
35489 *
35490 */
35491 Ext.define('Ext.ActionSheet', {
35492 extend: Ext.Sheet ,
35493 alias : 'widget.actionsheet',
35494
35495
35496 config: {
35497 /**
35498 * @cfg
35499 * @inheritdoc
35500 */
35501 baseCls: Ext.baseCSSPrefix + 'sheet-action',
35502
35503 /**
35504 * @cfg
35505 * @inheritdoc
35506 */
35507 left: 0,
35508
35509 /**
35510 * @cfg
35511 * @inheritdoc
35512 */
35513 right: 0,
35514
35515 /**
35516 * @cfg
35517 * @inheritdoc
35518 */
35519 bottom: 0,
35520
35521 // @hide
35522 centered: false,
35523
35524 /**
35525 * @cfg
35526 * @inheritdoc
35527 */
35528 height: 'auto',
35529
35530 /**
35531 * @cfg
35532 * @inheritdoc
35533 */
35534 defaultType: 'button'
35535 },
35536
35537 platformConfig: [{
35538 theme: ['Windows'],
35539 top: 0,
35540 bottom: null
35541 }]
35542 });
35543
35544 /**
35545 * The Connection class encapsulates a connection to the page's originating domain, allowing requests to be made either
35546 * to a configured URL, or to a URL specified at request time.
35547 *
35548 * Requests made by this class are asynchronous, and will return immediately. No data from the server will be available
35549 * to the statement immediately following the {@link #request} call. To process returned data, use a success callback
35550 * in the request options object, or an {@link #requestcomplete event listener}.
35551 *
35552 * # File Uploads
35553 *
35554 * File uploads are not performed using normal "Ajax" techniques, that is they are not performed using XMLHttpRequests.
35555 * Instead the form is submitted in the standard manner with the DOM `<form>` element temporarily modified to have its
35556 * target set to refer to a dynamically generated, hidden `<iframe>` which is inserted into the document but removed
35557 * after the return data has been gathered.
35558 *
35559 * The server response is parsed by the browser to create the document for the IFRAME. If the server is using JSON to
35560 * send the return object, then the Content-Type header must be set to "text/html" in order to tell the browser to
35561 * insert the text unchanged into the document body.
35562 *
35563 * Characters which are significant to an HTML parser must be sent as HTML entities, so encode `<` as `&lt;`, `&` as
35564 * `&amp;` etc.
35565 *
35566 * The response text is retrieved from the document, and a fake XMLHttpRequest object is created containing a
35567 * responseText property in order to conform to the requirements of event handlers and callbacks.
35568 *
35569 * Be aware that file upload packets are sent with the content type multipart/form and some server technologies
35570 * (notably JEE) may require some custom processing in order to retrieve parameter names and parameter values from the
35571 * packet content.
35572 *
35573 * __Note:__ It is not possible to check the response code of the hidden iframe, so the success handler will _always_ fire.
35574 */
35575 Ext.define('Ext.data.Connection', {
35576 mixins: {
35577 observable: Ext.mixin.Observable
35578 },
35579
35580 statics: {
35581 requestId: 0
35582 },
35583
35584 config: {
35585 /**
35586 * @cfg {String} url
35587 * The default URL to be used for requests to the server.
35588 * @accessor
35589 */
35590 url: null,
35591
35592 async: true,
35593
35594 /**
35595 * @cfg {String} [method=undefined]
35596 * The default HTTP method to be used for requests.
35597 *
35598 * __Note:__ This is case-sensitive and should be all caps.
35599 *
35600 * Defaults to `undefined`; if not set but params are present will use "POST", otherwise "GET".
35601 */
35602 method: null,
35603
35604 username: '',
35605 password: '',
35606
35607 /**
35608 * @cfg {Boolean} disableCaching
35609 * `true` to add a unique cache-buster param to GET requests.
35610 * @accessor
35611 */
35612 disableCaching: true,
35613
35614 /**
35615 * @cfg {String} disableCachingParam
35616 * Change the parameter which is sent went disabling caching through a cache buster.
35617 * @accessor
35618 */
35619 disableCachingParam: '_dc',
35620
35621 /**
35622 * @cfg {Number} timeout
35623 * The timeout in milliseconds to be used for requests.
35624 * @accessor
35625 */
35626 timeout : 30000,
35627
35628 /**
35629 * @cfg {Object} extraParams
35630 * Any parameters to be appended to the request.
35631 * @accessor
35632 */
35633 extraParams: null,
35634
35635 /**
35636 * @cfg {Object} defaultHeaders
35637 * An object containing request headers which are added to each request made by this object.
35638 * @accessor
35639 */
35640 defaultHeaders: null,
35641
35642 useDefaultHeader : true,
35643 defaultPostHeader : 'application/x-www-form-urlencoded; charset=UTF-8',
35644
35645 /**
35646 * @cfg {Boolean} useDefaultXhrHeader
35647 * Set this to false to not send the default Xhr header (X-Requested-With) with every request.
35648 * This should be set to false when making CORS (cross-domain) requests.
35649 * @accessor
35650 */
35651 useDefaultXhrHeader : true,
35652
35653 /**
35654 * @cfg {String} defaultXhrHeader
35655 * The value of the default Xhr header (X-Requested-With). This is only used when {@link #useDefaultXhrHeader}
35656 * is set to `true`.
35657 */
35658 defaultXhrHeader : 'XMLHttpRequest',
35659
35660 autoAbort: false
35661 },
35662
35663 textAreaRe: /textarea/i,
35664 multiPartRe: /multipart\/form-data/i,
35665 lineBreakRe: /\r\n/g,
35666
35667 constructor : function(config) {
35668 this.initConfig(config);
35669
35670 /**
35671 * @event beforerequest
35672 * Fires before a network request is made to retrieve a data object.
35673 * @param {Ext.data.Connection} conn This Connection object.
35674 * @param {Object} options The options config object passed to the {@link #request} method.
35675 */
35676 /**
35677 * @event requestcomplete
35678 * Fires if the request was successfully completed.
35679 * @param {Ext.data.Connection} conn This Connection object.
35680 * @param {Object} response The XHR object containing the response data.
35681 * See [The XMLHttpRequest Object](http://www.w3.org/TR/XMLHttpRequest/) for details.
35682 * @param {Object} options The options config object passed to the {@link #request} method.
35683 */
35684 /**
35685 * @event requestexception
35686 * Fires if an error HTTP status was returned from the server.
35687 * See [HTTP Status Code Definitions](http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html)
35688 * for details of HTTP status codes.
35689 * @param {Ext.data.Connection} conn This Connection object.
35690 * @param {Object} response The XHR object containing the response data.
35691 * See [The XMLHttpRequest Object](http://www.w3.org/TR/XMLHttpRequest/) for details.
35692 * @param {Object} options The options config object passed to the {@link #request} method.
35693 */
35694 this.requests = {};
35695 },
35696
35697 /**
35698 * Sends an HTTP request to a remote server.
35699 *
35700 * **Important:** Ajax server requests are asynchronous, and this call will
35701 * return before the response has been received. Process any returned data
35702 * in a callback function.
35703 *
35704 * Ext.Ajax.request({
35705 * url: 'ajax_demo/sample.json',
35706 * success: function(response, opts) {
35707 * var obj = Ext.decode(response.responseText);
35708 * console.dir(obj);
35709 * },
35710 * failure: function(response, opts) {
35711 * console.log('server-side failure with status code ' + response.status);
35712 * }
35713 * });
35714 *
35715 * To execute a callback function in the correct scope, use the `scope` option.
35716 *
35717 * @param {Object} options An object which may contain the following properties:
35718 *
35719 * (The options object may also contain any other property which might be needed to perform
35720 * post-processing in a callback because it is passed to callback functions.)
35721 *
35722 * @param {String/Function} options.url The URL to which to send the request, or a function
35723 * to call which returns a URL string. The scope of the function is specified by the `scope` option.
35724 * Defaults to the configured `url`.
35725 *
35726 * @param {Object/String/Function} options.params An object containing properties which are
35727 * used as parameters to the request, a url encoded string or a function to call to get either. The scope
35728 * of the function is specified by the `scope` option.
35729 *
35730 * @param {String} options.method The HTTP method to use
35731 * for the request. Defaults to the configured method, or if no method was configured,
35732 * "GET" if no parameters are being sent, and "POST" if parameters are being sent.
35733 *
35734 * __Note:__ The method name is case-sensitive and should be all caps.
35735 *
35736 * @param {Function} options.callback The function to be called upon receipt of the HTTP response.
35737 * The callback is called regardless of success or failure and is passed the following parameters:
35738 * @param {Object} options.callback.options The parameter to the request call.
35739 * @param {Boolean} options.callback.success `true` if the request succeeded.
35740 * @param {Object} options.callback.response The XMLHttpRequest object containing the response data.
35741 * See [www.w3.org/TR/XMLHttpRequest/](http://www.w3.org/TR/XMLHttpRequest/) for details about
35742 * accessing elements of the response.
35743 *
35744 * @param {Function} options.success The function to be called upon success of the request.
35745 * The callback is passed the following parameters:
35746 * @param {Object} options.success.response The XMLHttpRequest object containing the response data.
35747 * @param {Object} options.success.options The parameter to the request call.
35748 *
35749 * @param {Function} options.failure The function to be called upon failure of the request.
35750 * The callback is passed the following parameters:
35751 * @param {Object} options.failure.response The XMLHttpRequest object containing the response data.
35752 * @param {Object} options.failure.options The parameter to the request call.
35753 *
35754 * @param {Object} options.scope The scope in which to execute the callbacks: The "this" object for
35755 * the callback function. If the `url`, or `params` options were specified as functions from which to
35756 * draw values, then this also serves as the scope for those function calls. Defaults to the browser
35757 * window.
35758 *
35759 * @param {Boolean} options.xhr2 Determines is this request should use XHR2 features like FormData,
35760 * UploadStatus, etc if they are avaiable.
35761 *
35762 * @param {Number} [options.timeout=30000] The timeout in milliseconds to be used for this request.
35763 *
35764 * @param {HTMLElement/HTMLElement/String} options.form The `<form>` Element or the id of the `<form>`
35765 * to pull parameters from.
35766 *
35767 * @param {Boolean} options.isUpload **Only meaningful when used with the `form` option.**
35768 *
35769 * True if the form object is a file upload (will be set automatically if the form was configured
35770 * with **`enctype`** `"multipart/form-data"`).
35771 *
35772 * File uploads are not performed using normal "Ajax" techniques, that is they are **not**
35773 * performed using XMLHttpRequests. Instead the form is submitted in the standard manner with the
35774 * DOM `<form>` element temporarily modified to have its [target][] set to refer to a dynamically
35775 * generated, hidden `<iframe>` which is inserted into the document but removed after the return data
35776 * has been gathered.
35777 *
35778 * The server response is parsed by the browser to create the document for the IFRAME. If the
35779 * server is using JSON to send the return object, then the [Content-Type][] header must be set to
35780 * "text/html" in order to tell the browser to insert the text unchanged into the document body.
35781 *
35782 * The response text is retrieved from the document, and a fake XMLHttpRequest object is created
35783 * containing a `responseText` property in order to conform to the requirements of event handlers
35784 * and callbacks.
35785 *
35786 * Be aware that file upload packets are sent with the content type [multipart/form][] and some server
35787 * technologies (notably JEE) may require some custom processing in order to retrieve parameter names
35788 * and parameter values from the packet content.
35789 *
35790 * [target]: http://www.w3.org/TR/REC-html40/present/frames.html#adef-target
35791 * [Content-Type]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.17
35792 * [multipart/form]: http://www.faqs.org/rfcs/rfc2388.html
35793 *
35794 * @param {Object} options.headers Request headers to set for the request.
35795 *
35796 * @param {Object} options.xmlData XML document to use for the post.
35797 *
35798 * __Note:__ This will be used instead
35799 * of params for the post data. Any params will be appended to the URL.
35800 *
35801 * @param {Object/String} options.jsonData JSON data to use as the post.
35802 *
35803 * __Note:__ This will be used
35804 * instead of params for the post data. Any params will be appended to the URL.
35805 *
35806 * @param {Array} options.binaryData An array of bytes to submit in binary form. Any params will be appended to the URL. Returned data will be assumed to be a byte array or ArrayBuffer and placed in responseBytes. Other response types (e.g. xml / json) will not be provided.
35807 *
35808 * __Note:__ This will be used instead
35809 * of params for the post data. Any params will be appended to the URL.
35810 *
35811 * @param {Boolean} options.disableCaching True to add a unique cache-buster param to GET requests.
35812 *
35813 * @return {Object/null} The request object. This may be used to cancel the request.
35814 */
35815 request: function(options) {
35816 options = options || {};
35817 var me = this,
35818 scope = options.scope || window,
35819 username = options.username || me.getUsername(),
35820 password = options.password || me.getPassword() || '',
35821 useXhr2 = options.xhr2 === true && Ext.feature.has.XHR2,
35822 async, requestOptions, request, headers, xhr;
35823
35824 if(!Ext.isEmpty(username) && !Ext.isEmpty(password, true) && Ext.isEmpty(options.withCredentials)){
35825 options.withCredentials = true;
35826 }
35827
35828 if (me.fireEvent('beforerequest', me, options) !== false) {
35829 requestOptions = me.setOptions(options, scope);
35830
35831 if (this.isFormUpload(options) === true) {
35832 this.upload(options.form, requestOptions.url, requestOptions.data, options);
35833 return null;
35834 }
35835
35836 // if autoabort is set, cancel the current transactions
35837 if (options.autoAbort === true || me.getAutoAbort()) {
35838 me.abort();
35839 }
35840
35841 // create a connection object
35842 xhr = this.getXhrInstance();
35843
35844 async = options.async !== false ? (options.async || me.getAsync()) : false;
35845
35846 // open the request
35847 if (username) {
35848 xhr.open(requestOptions.method, requestOptions.url, async, username, password);
35849 } else {
35850 xhr.open(requestOptions.method, requestOptions.url, async);
35851 }
35852
35853 headers = me.setupHeaders(xhr, options, requestOptions.data, requestOptions.params);
35854
35855 // create the transaction object
35856 request = {
35857 id: ++Ext.data.Connection.requestId,
35858 xhr: xhr,
35859 headers: headers,
35860 options: options,
35861 async: async,
35862 timeout: setTimeout(function() {
35863 request.timedout = true;
35864 me.abort(request);
35865 }, options.timeout || me.getTimeout())
35866 };
35867 me.requests[request.id] = request;
35868
35869
35870 // bind our onload/statechange listener
35871 if (async) {
35872 xhr[useXhr2 ? 'onload' : 'onreadystatechange'] = Ext.Function.bind(me.onStateChange, me, [request]);
35873 }
35874
35875 if(useXhr2) {
35876 xhr.onerror = Ext.Function.bind(me.onStateChange, me, [request]);
35877 }
35878
35879 if(options.progress) {
35880 xhr.onprogress = function(e) {
35881 if(options.progress.isProgressable) {
35882 if(e.total === 0 && options.progress.getDynamic()) {
35883 Ext.Logger.warn("Server is not configured to properly return Content-Length. Dynamic progress will be disabled");
35884 options.progress.setState.call(options.progress, "download");
35885 options.progress.setDynamic(false);
35886 xhr.onprogress = null;
35887 return;
35888 }
35889
35890 Ext.callback(options.progress.updateProgress, options.progress, [(e.loaded / e.total), "download"]);
35891
35892 if(e.total > 0 && !options.progress.getDynamic() && options.progress.getInitialConfig().dynamic) {
35893 options.progress.setDynamic(true);
35894 }
35895 }else if(Ext.isFunction(options.progress)) {
35896 Ext.callback(options.progress, options.progressScope || request, [e, "download"])
35897 }
35898 };
35899
35900 if(Ext.feature.has.XHRUploadProgress) {
35901 xhr.upload.onprogress = function (e){
35902 me.fireEvent('requestuploadprogress', me, request, e);
35903 if(options.progress.isProgressable) {
35904 Ext.callback(options.progress.updateProgress, options.progress, [(e.loaded / e.total), "upload"]);
35905 }else if(Ext.isFunction(options.progress)) {
35906 Ext.callback(options.progress, options.progressScope || request, [e, "upload"])
35907 }
35908 };
35909 }
35910
35911 if(options.progress.isProgressable) {
35912 if(!Ext.feature.has.XHRUploadProgress) options.progress.setDynamic(false);
35913 Ext.callback(options.progress.startProgress, options.progress);
35914 }
35915 }
35916
35917 // start the request!
35918 xhr.send(requestOptions.data);
35919
35920 if (!async) {
35921 return this.onComplete(request);
35922 }
35923 return request;
35924 } else {
35925 Ext.callback(options.callback, options.scope, [options, undefined, undefined]);
35926 return null;
35927 }
35928 },
35929
35930 /**
35931 * Uploads a form using a hidden iframe.
35932 * @param {String/HTMLElement/Ext.Element} form The form to upload.
35933 * @param {String} url The url to post to.
35934 * @param {String} params Any extra parameters to pass.
35935 * @param {Object} options The initial options.
35936 */
35937 upload: function(form, url, params, options) {
35938 form = Ext.getDom(form);
35939 options = options || {};
35940
35941 var id = Ext.id(),
35942 me = this,
35943 frame = document.createElement('iframe'),
35944 hiddens = [],
35945 encoding = 'multipart/form-data',
35946 buf = {
35947 target: form.target,
35948 method: form.method,
35949 encoding: form.encoding,
35950 enctype: form.enctype,
35951 action: form.action
35952 }, addField = function(name, value) {
35953 hiddenItem = document.createElement('input');
35954 Ext.fly(hiddenItem).set({
35955 type: 'hidden',
35956 value: value,
35957 name: name
35958 });
35959 form.appendChild(hiddenItem);
35960 hiddens.push(hiddenItem);
35961 }, hiddenItem;
35962
35963 /*
35964 * Originally this behavior was modified for Opera 10 to apply the secure URL after
35965 * the frame had been added to the document. It seems this has since been corrected in
35966 * Opera so the behavior has been reverted, the URL will be set before being added.
35967 */
35968 Ext.fly(frame).set({
35969 id: id,
35970 name: id,
35971 cls: Ext.baseCSSPrefix + 'hide-display',
35972 src: Ext.SSL_SECURE_URL
35973 });
35974
35975 document.body.appendChild(frame);
35976
35977 // This is required so that IE doesn't pop the response up in a new window.
35978 if (document.frames) {
35979 document.frames[id].name = id;
35980 }
35981
35982 Ext.fly(form).set({
35983 target: id,
35984 method: 'POST',
35985 enctype: encoding,
35986 encoding: encoding,
35987 action: url || buf.action
35988 });
35989
35990 // add dynamic params
35991 if (params) {
35992 Ext.iterate(Ext.Object.fromQueryString(params), function(name, value) {
35993 if (Ext.isArray(value)) {
35994 Ext.each(value, function(v) {
35995 addField(name, v);
35996 });
35997 } else {
35998 addField(name, value);
35999 }
36000 });
36001 }
36002
36003 frame.addEventListener('load',
36004 function() {
36005 Ext.callback(me.onUploadComplete, me, [frame, options, id]);
36006 frame.removeEventListener('load', arguments.callee)
36007 }
36008 );
36009 form.submit();
36010
36011 Ext.fly(form).set(buf);
36012 Ext.each(hiddens, function(h) {
36013 Ext.removeNode(h);
36014 });
36015 },
36016
36017 onUploadComplete : function(frame, options, id) {
36018 // bogus response object
36019 var response = {
36020 responseText: '',
36021 responseXML: null,
36022 request: {
36023 options: options
36024 }
36025 },
36026 doc, body, firstChild;
36027
36028 try {
36029 doc = (frame.contentWindow && frame.contentWindow.document) || frame.contentDocument || window.frames[id].document;
36030
36031 if (doc) {
36032 if (doc.hasOwnProperty('body') && doc.body) {
36033 body = doc.body;
36034 }
36035
36036 if (body) {
36037 firstChild = body.firstChild || {};
36038
36039 if (this.textAreaRe.test(firstChild.tagName)) { // json response wrapped in textarea
36040 response.responseText = firstChild.value;
36041 } else {
36042 response.responseText = firstChild.innerHTML;
36043 }
36044
36045 //in IE the document may still have a body even if returns XML.
36046 response.responseXML = body.XMLDocument;
36047 }
36048 }
36049 } catch (e) {
36050 response.success = false;
36051 response.message = 'Cross-Domain access is not permitted between frames. XHR2 is recommended for this type of request.';
36052 response.error = e;
36053 }
36054
36055 this.onAfterUploadComplete(response, frame, options);
36056 },
36057
36058 onAfterUploadComplete: function(response, frame, options) {
36059 var me = this;
36060
36061 me.fireEvent('requestcomplete', me, response, options);
36062
36063 Ext.callback(options.callback, options.scope, [options, true, response]);
36064
36065 setTimeout(function() {
36066 Ext.removeNode(frame);
36067 }, 100);
36068 },
36069
36070 /**
36071 * Detects whether the form is intended to be used for an upload.
36072 * @private
36073 */
36074 isFormUpload: function(options) {
36075 var form = this.getForm(options);
36076 if (form) {
36077 return (options.isUpload || (this.multiPartRe).test(form.getAttribute('enctype')));
36078 }
36079 return false;
36080 },
36081
36082 /**
36083 * Gets the form object from options.
36084 * @private
36085 * @param {Object} options The request options.
36086 * @return {HTMLElement/null} The form, `null` if not passed.
36087 */
36088 getForm: function(options) {
36089 return Ext.getDom(options.form) || null;
36090 },
36091
36092 /**
36093 * Sets various options such as the url, params for the request.
36094 * @param {Object} options The initial options.
36095 * @param {Object} scope The scope to execute in.
36096 * @return {Object} The params for the request.
36097 */
36098 setOptions: function(options, scope) {
36099 var me = this,
36100 params = options.params || {},
36101 extraParams = me.getExtraParams(),
36102 urlParams = options.urlParams,
36103 url = options.url || me.getUrl(),
36104 jsonData = options.jsonData,
36105 method,
36106 disableCache,
36107 data;
36108
36109 // allow params to be a method that returns the params object
36110 if (Ext.isFunction(params)) {
36111 params = params.call(scope, options);
36112 }
36113
36114 // allow url to be a method that returns the actual url
36115 if (Ext.isFunction(url)) {
36116 url = url.call(scope, options);
36117 }
36118
36119 url = this.setupUrl(options, url);
36120
36121 //<debug>
36122 if (!url) {
36123 Ext.Logger.error('No URL specified');
36124 }
36125 //</debug>
36126
36127 // check for xml or json data, and make sure json data is encoded
36128 data = options.data || options.rawData || options.binaryData || options.xmlData || jsonData || null;
36129 if (jsonData && !Ext.isPrimitive(jsonData)) {
36130 data = Ext.encode(data);
36131 }
36132
36133 // Check for binary data. Transform if needed
36134 if (options.binaryData) {
36135 //<debug>
36136 if (!Ext.isArray(options.binaryData) && !(options.binaryData instanceof Blob)) {
36137 Ext.Logger.warn("Binary submission data must be an array of byte values or a Blob! Instead got " + typeof(options.binaryData));
36138 }
36139 //</debug>
36140 if (data instanceof Array) {
36141 data = (new Uint8Array(options.binaryData));
36142 }
36143 if (data instanceof Uint8Array) {
36144 // Note: Newer chrome version (v22 and up) warn that it is deprecated to send the ArrayBuffer and to send the ArrayBufferView instead. For FF this fails so for now send the ArrayBuffer.
36145 data = data.buffer; // send the underlying buffer, not the view, since that's not supported on versions of chrome older than 22
36146 }
36147 }
36148
36149 // make sure params are a url encoded string and include any extraParams if specified
36150 if (Ext.isObject(params)) {
36151 params = Ext.Object.toQueryString(params);
36152 }
36153
36154 if (Ext.isObject(extraParams)) {
36155 extraParams = Ext.Object.toQueryString(extraParams);
36156 }
36157
36158 params = params + ((extraParams) ? ((params) ? '&' : '') + extraParams : '');
36159
36160 urlParams = Ext.isObject(urlParams) ? Ext.Object.toQueryString(urlParams) : urlParams;
36161
36162 params = this.setupParams(options, params);
36163
36164 // decide the proper method for this request
36165 method = (options.method || me.getMethod() || ((params || data) ? 'POST' : 'GET')).toUpperCase();
36166 this.setupMethod(options, method);
36167
36168
36169 disableCache = options.disableCaching !== false ? (options.disableCaching || me.getDisableCaching()) : false;
36170
36171 // append date to prevent caching
36172 if (disableCache) {
36173 url = Ext.urlAppend(url, (options.disableCachingParam || me.getDisableCachingParam()) + '=' + (new Date().getTime()));
36174 }
36175
36176 // if the method is get or there is json/xml data append the params to the url
36177 if ((method == 'GET' || data) && params) {
36178 url = Ext.urlAppend(url, params);
36179 params = null;
36180 }
36181
36182 // allow params to be forced into the url
36183 if (urlParams) {
36184 url = Ext.urlAppend(url, urlParams);
36185 }
36186
36187 return {
36188 url: url,
36189 method: method,
36190 data: data || params || null
36191 };
36192 },
36193
36194 /**
36195 * Template method for overriding url.
36196 * @private
36197 * @param {Object} options
36198 * @param {String} url
36199 * @return {String} The modified url
36200 */
36201 setupUrl: function(options, url) {
36202 var form = this.getForm(options);
36203 if (form) {
36204 url = url || form.action;
36205 }
36206 return url;
36207 },
36208
36209
36210 /**
36211 * Template method for overriding params.
36212 * @private
36213 * @param {Object} options
36214 * @param {String} params
36215 * @return {String} The modified params.
36216 */
36217 setupParams: function(options, params) {
36218 var form = this.getForm(options),
36219 serializedForm;
36220 if (form && !this.isFormUpload(options)) {
36221 serializedForm = Ext.Element.serializeForm(form);
36222 params = params ? (params + '&' + serializedForm) : serializedForm;
36223 }
36224 return params;
36225 },
36226
36227 /**
36228 * Template method for overriding method.
36229 * @private
36230 * @param {Object} options
36231 * @param {String} method
36232 * @return {String} The modified method.
36233 */
36234 setupMethod: function(options, method) {
36235 if (this.isFormUpload(options)) {
36236 return 'POST';
36237 }
36238 return method;
36239 },
36240
36241 /**
36242 * Setup all the headers for the request.
36243 * @private
36244 * @param {Object} xhr The xhr object.
36245 * @param {Object} options The options for the request.
36246 * @param {Object} data The data for the request.
36247 * @param {Object} params The params for the request.
36248 */
36249 setupHeaders: function(xhr, options, data, params) {
36250 var me = this,
36251 headers = Ext.apply({}, options.headers || {}, me.getDefaultHeaders() || {}),
36252 contentType = me.getDefaultPostHeader(),
36253 jsonData = options.jsonData,
36254 xmlData = options.xmlData,
36255 key,
36256 header;
36257
36258 if (!headers['Content-Type'] && (data || params)) {
36259 if (data) {
36260 if (options.rawData) {
36261 contentType = 'text/plain';
36262 } else {
36263 if (xmlData && Ext.isDefined(xmlData)) {
36264 contentType = 'text/xml';
36265 } else if (jsonData && Ext.isDefined(jsonData)) {
36266 contentType = 'application/json';
36267 }
36268 }
36269 }
36270 if (!(Ext.feature.has.XHR2 && data instanceof FormData)) {
36271 headers['Content-Type'] = contentType;
36272 }
36273 }
36274
36275 if (((me.getUseDefaultXhrHeader() && options.useDefaultXhrHeader !== false) || options.useDefaultXhrHeader) && !headers['X-Requested-With']) {
36276 headers['X-Requested-With'] = me.getDefaultXhrHeader();
36277 }
36278
36279 if(!Ext.isEmpty(options.username) && !Ext.isEmpty(options.password)) {
36280 headers['Authorization'] = "Basic " + btoa(options.username+":"+options.password);
36281 }
36282
36283 // set up all the request headers on the xhr object
36284 try {
36285 for (key in headers) {
36286 if (headers.hasOwnProperty(key)) {
36287 header = headers[key];
36288 xhr.setRequestHeader(key, header);
36289 }
36290
36291 }
36292 } catch(e) {
36293 me.fireEvent('exception', key, header);
36294 }
36295
36296 if(options.responseType) {
36297 try {
36298 xhr.responseType = options.responseType === "blob" && Ext.browser.is.Safari ? "arraybuffer" : options.responseType;
36299 } catch (e) {
36300 // nothing to do. We're still continuing with the request.
36301 }
36302 }
36303
36304 if (options.withCredentials) {
36305 xhr.withCredentials = options.withCredentials;
36306 }
36307
36308 return headers;
36309 },
36310
36311 /**
36312 * Creates the appropriate XHR transport for the browser.
36313 * @private
36314 */
36315 getXhrInstance: (function() {
36316 var options = [function() {
36317 return new XMLHttpRequest();
36318 }, function() {
36319 return new ActiveXObject('MSXML2.XMLHTTP.3.0');
36320 }, function() {
36321 return new ActiveXObject('MSXML2.XMLHTTP');
36322 }, function() {
36323 return new ActiveXObject('Microsoft.XMLHTTP');
36324 }], i = 0,
36325 len = options.length,
36326 xhr;
36327
36328 for (; i < len; ++i) {
36329 try {
36330 xhr = options[i];
36331 xhr();
36332 break;
36333 } catch(e) {
36334 }
36335 }
36336 return xhr;
36337 })(),
36338
36339 /**
36340 * Determines whether this object has a request outstanding.
36341 * @param {Object} request The request to check.
36342 * @return {Boolean} True if there is an outstanding request.
36343 */
36344 isLoading : function(request) {
36345 if (!(request && request.xhr)) {
36346 return false;
36347 }
36348 // if there is a connection and readyState is not 0 or 4
36349 var state = request.xhr.readyState;
36350 return !(state === 0 || state == 4);
36351 },
36352
36353 /**
36354 * Aborts any outstanding request.
36355 * @param {Object} request (Optional) Defaults to the last request.
36356 */
36357 abort : function(request) {
36358 var me = this,
36359 requests = me.requests,
36360 id;
36361
36362 if (request && me.isLoading(request)) {
36363 /*
36364 * Clear out the onreadystatechange here, this allows us
36365 * greater control, the browser may/may not fire the function
36366 * depending on a series of conditions.
36367 */
36368 request.xhr.onreadystatechange = null;
36369 request.xhr.abort();
36370 me.clearTimeout(request);
36371 if (!request.timedout) {
36372 request.aborted = true;
36373 }
36374 me.onComplete(request);
36375 me.cleanup(request);
36376 } else if (!request) {
36377 for (id in requests) {
36378 if (requests.hasOwnProperty(id)) {
36379 me.abort(requests[id]);
36380 }
36381 }
36382 }
36383 },
36384
36385 /**
36386 * Aborts all outstanding requests.
36387 */
36388 abortAll: function() {
36389 this.abort();
36390 },
36391
36392 /**
36393 * Fires when the state of the XHR changes.
36394 * @private
36395 * @param {Object} request The request
36396 */
36397 onStateChange : function(request) {
36398 if (request.xhr.readyState == 4) {
36399 this.clearTimeout(request);
36400 this.onComplete(request);
36401 this.cleanup(request);
36402 }
36403 },
36404
36405 /**
36406 * Clears the timeout on the request.
36407 * @private
36408 * @param {Object} The request
36409 */
36410 clearTimeout: function(request) {
36411 clearTimeout(request.timeout);
36412 delete request.timeout;
36413 },
36414
36415 /**
36416 * Cleans up any left over information from the request.
36417 * @private
36418 * @param {Object} The request.
36419 */
36420 cleanup: function(request) {
36421 request.xhr = null;
36422 delete request.xhr;
36423 },
36424
36425 /**
36426 * To be called when the request has come back from the server.
36427 * @private
36428 * @param {Object} request
36429 * @return {Object} The response.
36430 */
36431 onComplete : function(request) {
36432 var me = this,
36433 options = request.options,
36434 result,
36435 success,
36436 response;
36437
36438 try {
36439 result = me.parseStatus(request.xhr.status, request.xhr);
36440
36441 if (request.timedout) {
36442 result.success = false;
36443 }
36444 } catch (e) {
36445 // in some browsers we can't access the status if the readyState is not 4, so the request has failed
36446 result = {
36447 success : false,
36448 isException : false
36449 };
36450 }
36451 success = result.success;
36452
36453 if (success) {
36454 response = me.createResponse(request);
36455 me.fireEvent('requestcomplete', me, response, options);
36456 Ext.callback(options.success, options.scope, [response, options]);
36457 } else {
36458 if (result.isException || request.aborted || request.timedout) {
36459 response = me.createException(request);
36460 } else {
36461 response = me.createResponse(request);
36462 }
36463 me.fireEvent('requestexception', me, response, options);
36464 Ext.callback(options.failure, options.scope, [response, options]);
36465 }
36466 Ext.callback(options.callback, options.scope, [options, success, response]);
36467
36468 if(options.progress && options.progress.isProgressable) {
36469 Ext.callback(options.progress.endProgress, options.progress, [result]);
36470 }
36471
36472 delete me.requests[request.id];
36473 return response;
36474 },
36475
36476 /**
36477 * Checks if the response status was successful.
36478 * @param {Number} status The status code.
36479 * @param {XMLHttpRequest} xhr
36480 * @return {Object} An object containing success/status state.
36481 */
36482 parseStatus: function(status, xhr) {
36483 // see: https://prototype.lighthouseapp.com/projects/8886/tickets/129-ie-mangles-http-response-status-code-204-to-1223
36484 status = status == 1223 ? 204 : status;
36485
36486 var success = (status >= 200 && status < 300) || status == 304 || (status == 0 && xhr.responseText && xhr.responseText.length > 0),
36487 isException = false;
36488
36489 if (!success) {
36490 switch (status) {
36491 case 12002:
36492 case 12029:
36493 case 12030:
36494 case 12031:
36495 case 12152:
36496 case 13030:
36497 isException = true;
36498 break;
36499 }
36500 }
36501 return {
36502 success: success,
36503 isException: isException
36504 };
36505 },
36506
36507 /**
36508 * Creates the response object.
36509 * @private
36510 * @param {Object} request
36511 */
36512 createResponse : function(request) {
36513 var xhr = request.xhr,
36514 headers = {},
36515 lines, count, line, index, key, response,
36516
36517 binaryResponse = xhr.responseType === "blob" || xhr.responseType === "arraybuffer",
36518 textResponse = xhr.responseType === "text",
36519 documentResponse = xhr.responseType === "document";
36520
36521 //we need to make this check here because if a request times out an exception is thrown
36522 //when calling getAllResponseHeaders() because the response never came back to populate it
36523 if (request.timedout || request.aborted) {
36524 request.success = false;
36525 lines = [];
36526 } else {
36527 lines = xhr.getAllResponseHeaders().replace(this.lineBreakRe, '\n').split('\n');
36528 }
36529
36530 count = lines.length;
36531
36532 while (count--) {
36533 line = lines[count];
36534 index = line.indexOf(':');
36535 if (index >= 0) {
36536 key = line.substr(0, index).toLowerCase();
36537 if (line.charAt(index + 1) == ' ') {
36538 ++index;
36539 }
36540 headers[key] = line.substr(index + 1);
36541 }
36542 }
36543
36544 request.xhr = null;
36545 delete request.xhr;
36546 response = {
36547 request: request,
36548 requestId : request.id,
36549 status : xhr.status,
36550 statusText : xhr.statusText,
36551 getResponseHeader : function(header) {
36552 return headers[header.toLowerCase()];
36553 },
36554 getAllResponseHeaders : function() {
36555 return headers;
36556 },
36557 responseText : binaryResponse ? null : documentResponse ? null : xhr.responseText,
36558 responseXML : binaryResponse ? null : textResponse ? null : xhr.responseXML,
36559 responseBytes : binaryResponse ? xhr.response : null
36560 };
36561
36562 if(request.options.responseType === "blob" && xhr.responseType === "arraybuffer") {
36563 response.responseBytes = new Blob([response.responseBytes], {type:xhr.getResponseHeader("Content-Type")})
36564 }
36565
36566 // If we don't explicitly tear down the xhr reference, IE6/IE7 will hold this in the closure of the
36567 // functions created with getResponseHeader/getAllResponseHeaders
36568 xhr = null;
36569 return response;
36570 },
36571
36572 /**
36573 * Creates the exception object.
36574 * @private
36575 * @param {Object} request
36576 */
36577 createException : function(request) {
36578 return {
36579 request : request,
36580 requestId : request.id,
36581 status : request.aborted ? -1 : 0,
36582 statusText : request.aborted ? 'transaction aborted' : 'communication failure',
36583 aborted: request.aborted,
36584 timedout: request.timedout
36585 };
36586 }
36587 });
36588
36589 /**
36590 * A singleton instance of an {@link Ext.data.Connection}. This class
36591 * is used to communicate with your server side code. It can be used as follows:
36592 *
36593 * Ext.Ajax.request({
36594 * url: 'page.php',
36595 * params: {
36596 * id: 1
36597 * },
36598 * success: function(response){
36599 * var text = response.responseText;
36600 * // process server response here
36601 * }
36602 * });
36603 *
36604 * Default options for all requests can be set by changing a property on the Ext.Ajax class:
36605 *
36606 * Ext.Ajax.setTimeout(60000); // 60 seconds
36607 *
36608 * Any options specified in the request method for the Ajax request will override any
36609 * defaults set on the Ext.Ajax class. In the code sample below, the timeout for the
36610 * request will be 60 seconds.
36611 *
36612 * Ext.Ajax.setTimeout(120000); // 120 seconds
36613 * Ext.Ajax.request({
36614 * url: 'page.aspx',
36615 * timeout: 60000
36616 * });
36617 *
36618 * In general, this class will be used for all Ajax requests in your application.
36619 * The main reason for creating a separate {@link Ext.data.Connection} is for a
36620 * series of requests that share common settings that are different to all other
36621 * requests in the application.
36622 *
36623 * For more information about utilizing AJAX in Sencha Touch, please review
36624 * our [AJAX Guide](../../../core_concepts/using_ajax.html).
36625 */
36626 Ext.define('Ext.Ajax', {
36627 extend: Ext.data.Connection ,
36628 singleton: true,
36629
36630 /**
36631 * @property {Boolean} autoAbort
36632 * Whether a new request should abort any pending requests.
36633 */
36634 autoAbort : false
36635 });
36636
36637 /**
36638 * Ext.Anim is used to execute simple animations defined in {@link Ext.anims}. The {@link #run} method can take any of the
36639 * properties defined below.
36640 *
36641 * Ext.Anim.run(this, 'fade', {
36642 * out: false,
36643 * autoClear: true
36644 * });
36645 *
36646 * When using {@link Ext.Anim#run}, ensure you require {@link Ext.Anim} in your application. Either do this using {@link Ext#require}:
36647 *
36648 * Ext.requires('Ext.Anim');
36649 *
36650 * when using {@link Ext#setup}:
36651 *
36652 * Ext.setup({
36653 * requires: ['Ext.Anim'],
36654 * onReady: function() {
36655 * //do something
36656 * }
36657 * });
36658 *
36659 * or when using {@link Ext#application}:
36660 *
36661 * Ext.application({
36662 * requires: ['Ext.Anim'],
36663 * launch: function() {
36664 * //do something
36665 * }
36666 * });
36667 *
36668 * @singleton
36669 */
36670
36671 Ext.define('Ext.Anim', {
36672 isAnim: true,
36673
36674 /**
36675 * @cfg {Boolean} disableAnimations
36676 * `true` to disable animations.
36677 */
36678 disableAnimations: false,
36679
36680 defaultConfig: {
36681 /**
36682 * @cfg {Object} from
36683 * An object of CSS values which the animation begins with. If you define a CSS property here, you must also
36684 * define it in the {@link #to} config.
36685 */
36686 from: {},
36687
36688 /**
36689 * @cfg {Object} to
36690 * An object of CSS values which the animation ends with. If you define a CSS property here, you must also
36691 * define it in the {@link #from} config.
36692 */
36693 to: {},
36694
36695 /**
36696 * @cfg {Number} duration
36697 * Time in milliseconds for the animation to last.
36698 */
36699 duration: 250,
36700
36701 /**
36702 * @cfg {Number} delay Time to delay before starting the animation.
36703 */
36704 delay: 0,
36705
36706 /**
36707 * @cfg {String} easing
36708 * Valid values are 'ease', 'linear', ease-in', 'ease-out', 'ease-in-out', or a cubic-bezier curve as defined by CSS.
36709 */
36710 easing: 'ease-in-out',
36711
36712 /**
36713 * @cfg {Boolean} autoClear
36714 * `true` to remove all custom CSS defined in the {@link #to} config when the animation is over.
36715 */
36716 autoClear: true,
36717
36718 /**
36719 * @cfg {Boolean} out
36720 * `true` if you want the animation to slide out of the screen.
36721 */
36722 out: true,
36723
36724 /**
36725 * @cfg {String} direction
36726 * Valid values are: 'left', 'right', 'up', 'down', and `null`.
36727 */
36728 direction: null,
36729
36730 /**
36731 * @cfg {Boolean} reverse
36732 * `true` to reverse the animation direction. For example, if the animation direction was set to 'left', it would
36733 * then use 'right'.
36734 */
36735 reverse: false
36736 },
36737
36738 /**
36739 * @cfg {Function} before
36740 * Code to execute before starting the animation.
36741 */
36742
36743 /**
36744 * @cfg {Function} after
36745 * Code to execute after the animation ends.
36746 */
36747
36748 /**
36749 * @cfg {Object} scope
36750 * Scope to run the {@link #before} function in.
36751 */
36752
36753 opposites: {
36754 'left': 'right',
36755 'right': 'left',
36756 'up': 'down',
36757 'down': 'up'
36758 },
36759
36760 constructor: function(config) {
36761 config = Ext.apply({}, config || {}, this.defaultConfig);
36762 this.config = config;
36763
36764 this.callSuper([config]);
36765
36766 this.running = [];
36767 },
36768
36769 initConfig: function(el, runConfig) {
36770 var me = this,
36771 config = Ext.apply({}, runConfig || {}, me.config);
36772
36773 config.el = el = Ext.get(el);
36774
36775 if (config.reverse && me.opposites[config.direction]) {
36776 config.direction = me.opposites[config.direction];
36777 }
36778
36779 if (me.config.before) {
36780 me.config.before.call(config, el, config);
36781 }
36782
36783 if (runConfig.before) {
36784 runConfig.before.call(config.scope || config, el, config);
36785 }
36786
36787 return config;
36788 },
36789
36790 /**
36791 * @ignore
36792 */
36793 run: function(el, config) {
36794 el = Ext.get(el);
36795 config = config || {};
36796
36797
36798 var me = this,
36799 style = el.dom.style,
36800 property,
36801 after = config.after;
36802
36803 if (me.running[el.id]) {
36804 me.onTransitionEnd(null, el, {
36805 config: config,
36806 after: after
36807 });
36808 }
36809
36810 config = this.initConfig(el, config);
36811
36812 if (this.disableAnimations) {
36813 for (property in config.to) {
36814 if (!config.to.hasOwnProperty(property)) {
36815 continue;
36816 }
36817 style[property] = config.to[property];
36818 }
36819 this.onTransitionEnd(null, el, {
36820 config: config,
36821 after: after
36822 });
36823 return me;
36824 }
36825
36826 el.un('transitionend', me.onTransitionEnd, me);
36827
36828 style.webkitTransitionDuration = '0ms';
36829 for (property in config.from) {
36830 if (!config.from.hasOwnProperty(property)) {
36831 continue;
36832 }
36833 style[property] = config.from[property];
36834 }
36835
36836 setTimeout(function() {
36837 // If this element has been destroyed since the timeout started, do nothing
36838 if (!el.dom) {
36839 return;
36840 }
36841
36842 // If this is a 3d animation we have to set the perspective on the parent
36843 if (config.is3d === true) {
36844 el.parent().setStyle({
36845 // See https://sencha.jira.com/browse/TOUCH-1498
36846 '-webkit-perspective': '1200',
36847 '-webkit-transform-style': 'preserve-3d'
36848 });
36849 }
36850
36851 style.webkitTransitionDuration = config.duration + 'ms';
36852 style.webkitTransitionProperty = 'all';
36853 style.webkitTransitionTimingFunction = config.easing;
36854
36855 // Bind our listener that fires after the animation ends
36856 el.on('transitionend', me.onTransitionEnd, me, {
36857 single: true,
36858 config: config,
36859 after: after
36860 });
36861
36862 for (property in config.to) {
36863 if (!config.to.hasOwnProperty(property)) {
36864 continue;
36865 }
36866 style[property] = config.to[property];
36867 }
36868 }, config.delay || 5);
36869
36870 me.running[el.id] = config;
36871 return me;
36872 },
36873
36874 onTransitionEnd: function(ev, el, o) {
36875 el = Ext.get(el);
36876
36877 if (this.running[el.id] === undefined) {
36878 return;
36879 }
36880
36881 var style = el.dom.style,
36882 config = o.config,
36883 me = this,
36884 property;
36885
36886 if (config.autoClear) {
36887 for (property in config.to) {
36888 if (!config.to.hasOwnProperty(property) || config[property] === false) {
36889 continue;
36890 }
36891 style[property] = '';
36892 }
36893 }
36894
36895 style.webkitTransitionDuration = null;
36896 style.webkitTransitionProperty = null;
36897 style.webkitTransitionTimingFunction = null;
36898
36899 if (config.is3d) {
36900 el.parent().setStyle({
36901 '-webkit-perspective': '',
36902 '-webkit-transform-style': ''
36903 });
36904 }
36905
36906 if (me.config.after) {
36907 me.config.after.call(config, el, config);
36908 }
36909
36910 if (o.after) {
36911 o.after.call(config.scope || me, el, config);
36912 }
36913
36914 delete me.running[el.id];
36915 }
36916 }, function() {
36917
36918 Ext.Anim.seed = 1000;
36919
36920 /**
36921 * Used to run an animation on a specific element. Use the config argument to customize the animation.
36922 * @param {Ext.Element/HTMLElement} el The element to animate.
36923 * @param {String} anim The animation type, defined in {@link Ext.anims}.
36924 * @param {Object} config The config object for the animation.
36925 * @method run
36926 */
36927 Ext.Anim.run = function(el, anim, config) {
36928 if (el.isComponent) {
36929 el = el.element;
36930 } else {
36931 el = Ext.get(el);
36932 }
36933
36934 config = config || {};
36935
36936 if (anim.isAnim) {
36937 anim.run(el, config);
36938 }
36939 else {
36940 if (Ext.isObject(anim)) {
36941 if (config.before && anim.before) {
36942 config.before = Ext.createInterceptor(config.before, anim.before, anim.scope);
36943 }
36944 if (config.after && anim.after) {
36945 config.after = Ext.createInterceptor(config.after, anim.after, anim.scope);
36946 }
36947 config = Ext.apply({}, config, anim);
36948 anim = anim.type;
36949 }
36950
36951 if (!Ext.anims[anim]) {
36952 throw anim + ' is not a valid animation type.';
36953 }
36954 else {
36955 // add el check to make sure dom exists.
36956 if (el && el.dom) {
36957 Ext.anims[anim].run(el, config);
36958 }
36959 }
36960 }
36961 };
36962
36963 /**
36964 * @class Ext.anims
36965 * Defines different types of animations.
36966 *
36967 * __Note:__ _flip_, _cube_, and _wipe_ animations do not work on Android.
36968 *
36969 * Please refer to {@link Ext.Anim} on how to use animations.
36970 * @singleton
36971 */
36972 Ext.anims = {
36973 /**
36974 * Fade Animation
36975 */
36976 fade: new Ext.Anim({
36977 type: 'fade',
36978 before: function(el) {
36979 var fromOpacity = 1,
36980 toOpacity = 1,
36981 curZ = el.getStyle('z-index') == 'auto' ? 0 : el.getStyle('z-index'),
36982 zIndex = curZ;
36983
36984 if (this.out) {
36985 toOpacity = 0;
36986 } else {
36987 zIndex = Math.abs(curZ) + 1;
36988 fromOpacity = 0;
36989 }
36990
36991 this.from = {
36992 'opacity': fromOpacity,
36993 'z-index': zIndex
36994 };
36995 this.to = {
36996 'opacity': toOpacity,
36997 'z-index': zIndex
36998 };
36999 }
37000 }),
37001
37002 /**
37003 * Slide Animation
37004 */
37005 slide: new Ext.Anim({
37006 direction: 'left',
37007 cover: false,
37008 reveal: false,
37009 opacity: false,
37010 'z-index': false,
37011
37012 before: function(el) {
37013 var currentZIndex = el.getStyle('z-index') == 'auto' ? 0 : el.getStyle('z-index'),
37014 currentOpacity = el.getStyle('opacity'),
37015 zIndex = currentZIndex + 1,
37016 out = this.out,
37017 direction = this.direction,
37018 toX = 0,
37019 toY = 0,
37020 fromX = 0,
37021 fromY = 0,
37022 elH = el.getHeight(),
37023 elW = el.getWidth();
37024
37025 if (direction == 'left' || direction == 'right') {
37026 if (out) {
37027 toX = -elW;
37028 }
37029 else {
37030 fromX = elW;
37031 }
37032 }
37033 else if (direction == 'up' || direction == 'down') {
37034 if (out) {
37035 toY = -elH;
37036 }
37037 else {
37038 fromY = elH;
37039 }
37040 }
37041
37042 if (direction == 'right' || direction == 'down') {
37043 toY *= -1;
37044 toX *= -1;
37045 fromY *= -1;
37046 fromX *= -1;
37047 }
37048
37049 if (this.cover && out) {
37050 toX = 0;
37051 toY = 0;
37052 zIndex = currentZIndex;
37053 }
37054 else if (this.reveal && !out) {
37055 fromX = 0;
37056 fromY = 0;
37057 zIndex = currentZIndex;
37058 }
37059
37060 this.from = {
37061 '-webkit-transform': 'translate3d(' + fromX + 'px, ' + fromY + 'px, 0)',
37062 'z-index': zIndex,
37063 'opacity': currentOpacity - 0.01
37064 };
37065 this.to = {
37066 '-webkit-transform': 'translate3d(' + toX + 'px, ' + toY + 'px, 0)',
37067 'z-index': zIndex,
37068 'opacity': currentOpacity
37069 };
37070 }
37071 }),
37072
37073 /**
37074 * Pop Animation
37075 */
37076 pop: new Ext.Anim({
37077 scaleOnExit: true,
37078 before: function(el) {
37079 var fromScale = 1,
37080 toScale = 1,
37081 fromOpacity = 1,
37082 toOpacity = 1,
37083 curZ = el.getStyle('z-index') == 'auto' ? 0 : el.getStyle('z-index'),
37084 fromZ = curZ,
37085 toZ = curZ;
37086
37087 if (!this.out) {
37088 fromScale = 0.01;
37089 fromZ = curZ + 1;
37090 toZ = curZ + 1;
37091 fromOpacity = 0;
37092 }
37093 else {
37094 if (this.scaleOnExit) {
37095 toScale = 0.01;
37096 toOpacity = 0;
37097 } else {
37098 toOpacity = 0.8;
37099 }
37100 }
37101
37102 this.from = {
37103 '-webkit-transform': 'scale(' + fromScale + ')',
37104 '-webkit-transform-origin': '50% 50%',
37105 'opacity': fromOpacity,
37106 'z-index': fromZ
37107 };
37108
37109 this.to = {
37110 '-webkit-transform': 'scale(' + toScale + ')',
37111 '-webkit-transform-origin': '50% 50%',
37112 'opacity': toOpacity,
37113 'z-index': toZ
37114 };
37115 }
37116 }),
37117
37118 /**
37119 * Flip Animation
37120 */
37121 flip: new Ext.Anim({
37122 is3d: true,
37123 direction: 'left',
37124 before: function(el) {
37125 var rotateProp = 'Y',
37126 fromScale = 1,
37127 toScale = 1,
37128 fromRotate = 0,
37129 toRotate = 0;
37130
37131 if (this.out) {
37132 toRotate = -180;
37133 toScale = 0.8;
37134 }
37135 else {
37136 fromRotate = 180;
37137 fromScale = 0.8;
37138 }
37139
37140 if (this.direction == 'up' || this.direction == 'down') {
37141 rotateProp = 'X';
37142 }
37143
37144 if (this.direction == 'right' || this.direction == 'left') {
37145 toRotate *= -1;
37146 fromRotate *= -1;
37147 }
37148
37149 this.from = {
37150 '-webkit-transform': 'rotate' + rotateProp + '(' + fromRotate + 'deg) scale(' + fromScale + ')',
37151 '-webkit-backface-visibility': 'hidden'
37152 };
37153 this.to = {
37154 '-webkit-transform': 'rotate' + rotateProp + '(' + toRotate + 'deg) scale(' + toScale + ')',
37155 '-webkit-backface-visibility': 'hidden'
37156 };
37157 }
37158 }),
37159
37160 /**
37161 * Cube Animation
37162 */
37163 cube: new Ext.Anim({
37164 is3d: true,
37165 direction: 'left',
37166 style: 'outer',
37167 before: function(el) {
37168 var origin = '0% 0%',
37169 fromRotate = 0,
37170 toRotate = 0,
37171 rotateProp = 'Y',
37172 fromZ = 0,
37173 toZ = 0,
37174 elW = el.getWidth(),
37175 elH = el.getHeight(),
37176 showTranslateZ = true,
37177 fromTranslate = ' translateX(0)',
37178 toTranslate = '';
37179
37180 if (this.direction == 'left' || this.direction == 'right') {
37181 if (this.out) {
37182 origin = '100% 100%';
37183 toZ = elW;
37184 toRotate = -90;
37185 } else {
37186 origin = '0% 0%';
37187 fromZ = elW;
37188 fromRotate = 90;
37189 }
37190 } else if (this.direction == 'up' || this.direction == 'down') {
37191 rotateProp = 'X';
37192 if (this.out) {
37193 origin = '100% 100%';
37194 toZ = elH;
37195 toRotate = 90;
37196 } else {
37197 origin = '0% 0%';
37198 fromZ = elH;
37199 fromRotate = -90;
37200 }
37201 }
37202
37203 if (this.direction == 'down' || this.direction == 'right') {
37204 fromRotate *= -1;
37205 toRotate *= -1;
37206 origin = (origin == '0% 0%') ? '100% 100%': '0% 0%';
37207 }
37208
37209 if (this.style == 'inner') {
37210 fromZ *= -1;
37211 toZ *= -1;
37212 fromRotate *= -1;
37213 toRotate *= -1;
37214
37215 if (!this.out) {
37216 toTranslate = ' translateX(0px)';
37217 origin = '0% 50%';
37218 } else {
37219 toTranslate = fromTranslate;
37220 origin = '100% 50%';
37221 }
37222 }
37223
37224 this.from = {
37225 '-webkit-transform': 'rotate' + rotateProp + '(' + fromRotate + 'deg)' + (showTranslateZ ? ' translateZ(' + fromZ + 'px)': '') + fromTranslate,
37226 '-webkit-transform-origin': origin
37227 };
37228 this.to = {
37229 '-webkit-transform': 'rotate' + rotateProp + '(' + toRotate + 'deg) translateZ(' + toZ + 'px)' + toTranslate,
37230 '-webkit-transform-origin': origin
37231 };
37232 },
37233 duration: 250
37234 }),
37235
37236
37237 /**
37238 * Wipe Animation.
37239 * Because of the amount of calculations involved, this animation is best used on small display
37240 * changes or specifically for phone environments. Does not currently accept any parameters.
37241 */
37242 wipe: new Ext.Anim({
37243 before: function(el) {
37244 var curZ = el.getStyle('z-index'),
37245 zIndex,
37246 mask = '';
37247
37248 if (!this.out) {
37249 zIndex = curZ + 1;
37250 mask = '-webkit-gradient(linear, left bottom, right bottom, from(transparent), to(#000), color-stop(66%, #000), color-stop(33%, transparent))';
37251
37252 this.from = {
37253 '-webkit-mask-image': mask,
37254 '-webkit-mask-size': el.getWidth() * 3 + 'px ' + el.getHeight() + 'px',
37255 'z-index': zIndex,
37256 '-webkit-mask-position-x': 0
37257 };
37258 this.to = {
37259 '-webkit-mask-image': mask,
37260 '-webkit-mask-size': el.getWidth() * 3 + 'px ' + el.getHeight() + 'px',
37261 'z-index': zIndex,
37262 '-webkit-mask-position-x': -el.getWidth() * 2 + 'px'
37263 };
37264 }
37265 },
37266 duration: 500
37267 })
37268 };
37269 });
37270
37271 /**
37272 * Provides a base class for audio/visual controls. Should not be used directly.
37273 *
37274 * Please see the {@link Ext.Audio} and {@link Ext.Video} classes for more information.
37275 * @private
37276 */
37277 Ext.define('Ext.Media', {
37278 extend: Ext.Component ,
37279 xtype: 'media',
37280
37281 /**
37282 * @event play
37283 * Fires whenever the media is played.
37284 * @param {Ext.Media} this
37285 */
37286
37287 /**
37288 * @event pause
37289 * Fires whenever the media is paused.
37290 * @param {Ext.Media} this
37291 * @param {Number} time The time at which the media was paused at in seconds.
37292 */
37293
37294 /**
37295 * @event ended
37296 * Fires whenever the media playback has ended.
37297 * @param {Ext.Media} this
37298 * @param {Number} time The time at which the media ended at in seconds.
37299 */
37300
37301 /**
37302 * @event stop
37303 * Fires whenever the media is stopped.
37304 * The `pause` event will also fire after the `stop` event if the media is currently playing.
37305 * The `timeupdate` event will also fire after the `stop` event regardless of playing status.
37306 * @param {Ext.Media} this
37307 */
37308
37309 /**
37310 * @event volumechange
37311 * Fires whenever the volume is changed.
37312 * @param {Ext.Media} this
37313 * @param {Number} volume The volume level from 0 to 1.
37314 */
37315
37316 /**
37317 * @event mutedchange
37318 * Fires whenever the muted status is changed.
37319 * The volumechange event will also fire after the `mutedchange` event fires.
37320 * @param {Ext.Media} this
37321 * @param {Boolean} muted The muted status.
37322 */
37323
37324 /**
37325 * @event timeupdate
37326 * Fires when the media is playing every 15 to 250ms.
37327 * @param {Ext.Media} this
37328 * @param {Number} time The current time in seconds.
37329 */
37330
37331 config: {
37332 /**
37333 * @cfg {String} url
37334 * Location of the media to play.
37335 * @accessor
37336 */
37337 url: '',
37338
37339 /**
37340 * @cfg {Boolean} enableControls
37341 * Set this to `false` to turn off the native media controls.
37342 * Defaults to `false` when you are on Android, as it doesn't support controls.
37343 * @accessor
37344 */
37345 enableControls: Ext.os.is.Android ? false : true,
37346
37347 /**
37348 * @cfg {Boolean} autoResume
37349 * Will automatically start playing the media when the container is activated.
37350 * @accessor
37351 */
37352 autoResume: false,
37353
37354 /**
37355 * @cfg {Boolean} autoPause
37356 * Will automatically pause the media when the container is deactivated.
37357 * @accessor
37358 */
37359 autoPause: true,
37360
37361 /**
37362 * @cfg {Boolean} preload
37363 * Will begin preloading the media immediately.
37364 * @accessor
37365 */
37366 preload: true,
37367
37368 /**
37369 * @cfg {Boolean} loop
37370 * Will loop the media forever.
37371 * @accessor
37372 */
37373 loop: false,
37374
37375 /**
37376 * @cfg {Ext.Element} media
37377 * A reference to the underlying audio/video element.
37378 * @accessor
37379 */
37380 media: null,
37381
37382 /**
37383 * @cfg {Number} volume
37384 * The volume of the media from 0.0 to 1.0.
37385 * @accessor
37386 */
37387 volume: 1,
37388
37389 /**
37390 * @cfg {Boolean} muted
37391 * Whether or not the media is muted. This will also set the volume to zero.
37392 * @accessor
37393 */
37394 muted: false
37395 },
37396
37397 constructor: function() {
37398 this.mediaEvents = {};
37399 this.callSuper(arguments);
37400 },
37401
37402 initialize: function() {
37403 var me = this;
37404 me.callParent();
37405
37406 me.on({
37407 scope: me,
37408
37409 activate : me.onActivate,
37410 deactivate: me.onDeactivate
37411 });
37412
37413 me.addMediaListener({
37414 canplay: 'onCanPlay',
37415 play: 'onPlay',
37416 pause: 'onPause',
37417 ended: 'onEnd',
37418 volumechange: 'onVolumeChange',
37419 timeupdate: 'onTimeUpdate'
37420 });
37421 },
37422
37423 addMediaListener: function(event, fn) {
37424 var me = this,
37425 dom = me.media.dom,
37426 bind = Ext.Function.bind;
37427
37428 Ext.Object.each(event, function(e, fn) {
37429 fn = bind(me[fn], me);
37430 me.mediaEvents[e] = fn;
37431 dom.addEventListener(e, fn);
37432 });
37433 },
37434
37435 onPlay: function() {
37436 this.fireEvent('play', this);
37437 },
37438
37439 onCanPlay: function() {
37440 this.fireEvent('canplay', this);
37441 },
37442
37443 onPause: function() {
37444 this.fireEvent('pause', this, this.getCurrentTime());
37445 },
37446
37447 onEnd: function() {
37448 this.fireEvent('ended', this, this.getCurrentTime());
37449 },
37450
37451 onVolumeChange: function() {
37452 this.fireEvent('volumechange', this, this.media.dom.volume);
37453 },
37454
37455 onTimeUpdate: function() {
37456 this.fireEvent('timeupdate', this, this.getCurrentTime());
37457 },
37458
37459 /**
37460 * Returns if the media is currently playing.
37461 * @return {Boolean} playing `true` if the media is playing.
37462 */
37463 isPlaying: function() {
37464 return !Boolean(this.media.dom.paused);
37465 },
37466
37467 // @private
37468 onActivate: function() {
37469 var me = this;
37470
37471 if (me.getAutoResume() && !me.isPlaying()) {
37472 me.play();
37473 }
37474 },
37475
37476 // @private
37477 onDeactivate: function() {
37478 var me = this;
37479
37480 if (me.getAutoPause() && me.isPlaying()) {
37481 me.pause();
37482 }
37483 },
37484
37485 /**
37486 * Sets the URL of the media element. If the media element already exists, it is update the src attribute of the
37487 * element. If it is currently playing, it will start the new video.
37488 */
37489 updateUrl: function(newUrl) {
37490 var dom = this.media.dom;
37491
37492 //when changing the src, we must call load:
37493 //http://developer.apple.com/library/safari/#documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/ControllingMediaWithJavaScript/ControllingMediaWithJavaScript.html
37494
37495 dom.src = newUrl;
37496
37497 if ('load' in dom) {
37498 dom.load();
37499 }
37500
37501 if (this.isPlaying()) {
37502 this.play();
37503 }
37504 },
37505
37506 /**
37507 * Updates the controls of the video element.
37508 */
37509 updateEnableControls: function(enableControls) {
37510 this.media.dom.controls = enableControls ? 'controls' : false;
37511 },
37512
37513 /**
37514 * Updates the loop setting of the media element.
37515 */
37516 updateLoop: function(loop) {
37517 this.media.dom.loop = loop ? 'loop' : false;
37518 },
37519
37520 /**
37521 * Starts or resumes media playback.
37522 */
37523 play: function() {
37524 var dom = this.media.dom;
37525
37526 if ('play' in dom) {
37527 dom.play();
37528 setTimeout(function() {
37529 dom.play();
37530 }, 10);
37531 }
37532 },
37533
37534 /**
37535 * Pauses media playback.
37536 */
37537 pause: function() {
37538 var dom = this.media.dom;
37539
37540 if ('pause' in dom) {
37541 dom.pause();
37542 }
37543 },
37544
37545 /**
37546 * Toggles the media playback state.
37547 */
37548 toggle: function() {
37549 if (this.isPlaying()) {
37550 this.pause();
37551 } else {
37552 this.play();
37553 }
37554 },
37555
37556 /**
37557 * Stops media playback and returns to the beginning.
37558 */
37559 stop: function() {
37560 var me = this;
37561
37562 me.setCurrentTime(0);
37563 me.fireEvent('stop', me);
37564 me.pause();
37565 },
37566
37567 //@private
37568 updateVolume: function(volume) {
37569 this.media.dom.volume = volume;
37570 },
37571
37572 //@private
37573 updateMuted: function(muted) {
37574 this.fireEvent('mutedchange', this, muted);
37575
37576 this.media.dom.muted = muted;
37577 },
37578
37579 /**
37580 * Returns the current time of the media, in seconds.
37581 * @return {Number}
37582 */
37583 getCurrentTime: function() {
37584 return this.media.dom.currentTime;
37585 },
37586
37587 /*
37588 * Set the current time of the media.
37589 * @param {Number} time The time, in seconds.
37590 * @return {Number}
37591 */
37592 setCurrentTime: function(time) {
37593 this.media.dom.currentTime = time;
37594
37595 return time;
37596 },
37597
37598 /**
37599 * Returns the duration of the media, in seconds.
37600 * @return {Number}
37601 */
37602 getDuration: function() {
37603 return this.media.dom.duration;
37604 },
37605
37606 destroy: function() {
37607 var me = this,
37608 dom = me.media.dom,
37609 mediaEvents = me.mediaEvents;
37610
37611 Ext.Object.each(mediaEvents, function(event, fn) {
37612 dom.removeEventListener(event, fn);
37613 });
37614
37615 this.callSuper();
37616 }
37617 });
37618
37619 /**
37620 * {@link Ext.Audio} is a simple class which provides a container for the
37621 * [HTML5 Audio element](http://developer.mozilla.org/en-US/docs/Using_HTML5_audio_and_video).
37622 *
37623 * ## Recommended File Types/Compression:
37624 *
37625 * * Uncompressed WAV and AIF audio
37626 * * MP3 audio
37627 * * AAC-LC
37628 * * HE-AAC audio
37629 *
37630 * ## Notes
37631 *
37632 * On Android devices, the audio tags controls do not show. You must use the {@link #method-play},
37633 * {@link #method-pause}, and {@link #toggle} methods to control the audio (example below).
37634 *
37635 * ## Examples
37636 *
37637 * This example shows the use of the {@link Ext.Audio} component in a fullscreen container--change
37638 * the url: item for the location of an audio file--note that the audio starts on page load:
37639 *
37640 * @example preview
37641 * Ext.create('Ext.Container', {
37642 * fullscreen: true,
37643 * layout: {
37644 * type : 'vbox',
37645 * pack : 'center',
37646 * align: 'stretch'
37647 * },
37648 * items: [
37649 * {
37650 * xtype : 'toolbar',
37651 * docked: 'top',
37652 * title : 'Ext.Audio'
37653 * },
37654 * {
37655 * xtype: 'audio',
37656 * url : 'touch-build/examples/audio/crash.mp3'
37657 * }
37658 * ]
37659 * });
37660 *
37661 * You can also set the {@link #hidden} configuration of the {@link Ext.Audio} component to true by default,
37662 * and then control the audio by using the {@link #method-play}, {@link #method-pause}, and {@link #toggle} methods:
37663 *
37664 * @example preview
37665 * Ext.create('Ext.Container', {
37666 * fullscreen: true,
37667 * layout: {
37668 * type: 'vbox',
37669 * pack: 'center'
37670 * },
37671 * items: [
37672 * {
37673 * xtype : 'toolbar',
37674 * docked: 'top',
37675 * title : 'Ext.Audio'
37676 * },
37677 * {
37678 * xtype: 'toolbar',
37679 * docked: 'bottom',
37680 * defaults: {
37681 * xtype: 'button',
37682 * handler: function() {
37683 * var container = this.getParent().getParent(),
37684 * // use ComponentQuery to get the audio component (using its xtype)
37685 * audio = container.down('audio');
37686 *
37687 * audio.toggle();
37688 * this.setText(audio.isPlaying() ? 'Pause' : 'Play');
37689 * }
37690 * },
37691 * items: [
37692 * { text: 'Play', flex: 1 }
37693 * ]
37694 * },
37695 * {
37696 * html: 'Hidden audio!',
37697 * styleHtmlContent: true
37698 * },
37699 * {
37700 * xtype : 'audio',
37701 * hidden: true,
37702 * url : 'touch-build/examples/audio/crash.mp3'
37703 * }
37704 * ]
37705 * });
37706 */
37707 Ext.define('Ext.Audio', {
37708 extend: Ext.Media ,
37709 xtype : 'audio',
37710
37711 config: {
37712 /**
37713 * @cfg
37714 * @inheritdoc
37715 */
37716 cls: Ext.baseCSSPrefix + 'audio'
37717
37718 /**
37719 * @cfg {String} url
37720 * The location of the audio to play.
37721 *
37722 * ### Recommended file types are:
37723 * * Uncompressed WAV and AIF audio
37724 * * MP3 audio
37725 * * AAC-LC
37726 * * HE-AAC audio
37727 * @accessor
37728 */
37729 },
37730
37731 // @private
37732 onActivate: function() {
37733 var me = this;
37734
37735 me.callParent();
37736
37737 if (Ext.os.is.Phone) {
37738 me.element.show();
37739 }
37740 },
37741
37742 // @private
37743 onDeactivate: function() {
37744 var me = this;
37745
37746 me.callParent();
37747
37748 if (Ext.os.is.Phone) {
37749 me.element.hide();
37750 }
37751 },
37752
37753 template: [{
37754 reference: 'media',
37755 preload: 'auto',
37756 tag: 'audio',
37757 cls: Ext.baseCSSPrefix + 'component'
37758 }]
37759 });
37760
37761 /**
37762 * Provides a cross browser class for retrieving location information.
37763 *
37764 * Based on the [Geolocation API Specification](http://dev.w3.org/geo/api/spec-source.html)
37765 *
37766 * When instantiated, by default this class immediately begins tracking location information,
37767 * firing a {@link #locationupdate} event when new location information is available. To disable this
37768 * location tracking (which may be battery intensive on mobile devices), set {@link #autoUpdate} to `false`.
37769 *
37770 * When this is done, only calls to {@link #updateLocation} will trigger a location retrieval.
37771 *
37772 * A {@link #locationerror} event is raised when an error occurs retrieving the location, either due to a user
37773 * denying the application access to it, or the browser not supporting it.
37774 *
37775 * The below code shows a GeoLocation making a single retrieval of location information.
37776 *
37777 * var geo = Ext.create('Ext.util.Geolocation', {
37778 * autoUpdate: false,
37779 * listeners: {
37780 * locationupdate: function(geo) {
37781 * alert('New latitude: ' + geo.getLatitude());
37782 * },
37783 * locationerror: function(geo, bTimeout, bPermissionDenied, bLocationUnavailable, message) {
37784 * if(bTimeout){
37785 * alert('Timeout occurred.');
37786 * } else {
37787 * alert('Error occurred.');
37788 * }
37789 * }
37790 * }
37791 * });
37792 * geo.updateLocation();
37793 */
37794 Ext.define('Ext.util.Geolocation', {
37795 extend: Ext.Evented ,
37796 alternateClassName: ['Ext.util.GeoLocation'],
37797
37798 config: {
37799 /**
37800 * @event locationerror
37801 * Raised when a location retrieval operation failed.
37802 *
37803 * In the case of calling updateLocation, this event will be raised only once.
37804 *
37805 * If {@link #autoUpdate} is set to `true`, this event could be raised repeatedly.
37806 * The first error is relative to the moment {@link #autoUpdate} was set to `true`
37807 * (or this {@link Ext.util.Geolocation} was initialized with the {@link #autoUpdate} config option set to `true`).
37808 * Subsequent errors are relative to the moment when the device determines that it's position has changed.
37809 * @param {Ext.util.Geolocation} this
37810 * @param {Boolean} timeout
37811 * Boolean indicating a timeout occurred
37812 * @param {Boolean} permissionDenied
37813 * Boolean indicating the user denied the location request
37814 * @param {Boolean} locationUnavailable
37815 * Boolean indicating that the location of the device could not be determined.
37816 * For instance, one or more of the location providers used in the location acquisition
37817 * process reported an internal error that caused the process to fail entirely.
37818 * @param {String} message An error message describing the details of the error encountered.
37819 *
37820 * This attribute is primarily intended for debugging and should not be used
37821 * directly in an application user interface.
37822 */
37823
37824 /**
37825 * @event locationupdate
37826 * Raised when a location retrieval operation has been completed successfully.
37827 * @param {Ext.util.Geolocation} this
37828 * Retrieve the current location information from the GeoLocation object by using the read-only
37829 * properties: {@link #latitude}, {@link #longitude}, {@link #accuracy}, {@link #altitude}, {@link #altitudeAccuracy}, {@link #heading}, and {@link #speed}.
37830 */
37831
37832 /**
37833 * @cfg {Boolean} autoUpdate
37834 * When set to `true`, continually monitor the location of the device (beginning immediately)
37835 * and fire {@link #locationupdate} and {@link #locationerror} events.
37836 */
37837 autoUpdate: true,
37838
37839 /**
37840 * @cfg {Number} frequency
37841 * The frequency of each update if {@link #autoUpdate} is set to `true`.
37842 */
37843 frequency: 10000,
37844
37845 /**
37846 * Read-only property representing the last retrieved
37847 * geographical coordinate specified in degrees.
37848 * @type Number
37849 * @readonly
37850 */
37851 latitude: null,
37852
37853 /**
37854 * Read-only property representing the last retrieved
37855 * geographical coordinate specified in degrees.
37856 * @type Number
37857 * @readonly
37858 */
37859 longitude: null,
37860
37861 /**
37862 * Read-only property representing the last retrieved
37863 * accuracy level of the latitude and longitude coordinates,
37864 * specified in meters.
37865 *
37866 * This will always be a non-negative number.
37867 *
37868 * This corresponds to a 95% confidence level.
37869 * @type Number
37870 * @readonly
37871 */
37872 accuracy: null,
37873
37874 /**
37875 * Read-only property representing the last retrieved
37876 * height of the position, specified in meters above the ellipsoid
37877 * [WGS84](http://dev.w3.org/geo/api/spec-source.html#ref-wgs).
37878 * @type Number
37879 * @readonly
37880 */
37881 altitude: null,
37882
37883 /**
37884 * Read-only property representing the last retrieved
37885 * accuracy level of the altitude coordinate, specified in meters.
37886 *
37887 * If altitude is not null then this will be a non-negative number.
37888 * Otherwise this returns `null`.
37889 *
37890 * This corresponds to a 95% confidence level.
37891 * @type Number
37892 * @readonly
37893 */
37894 altitudeAccuracy: null,
37895
37896 /**
37897 * Read-only property representing the last retrieved
37898 * direction of travel of the hosting device,
37899 * specified in non-negative degrees between 0 and 359,
37900 * counting clockwise relative to the true north.
37901 *
37902 * If speed is 0 (device is stationary), then this returns `NaN`.
37903 * @type Number
37904 * @readonly
37905 */
37906 heading: null,
37907
37908 /**
37909 * Read-only property representing the last retrieved
37910 * current ground speed of the device, specified in meters per second.
37911 *
37912 * If this feature is unsupported by the device, this returns `null`.
37913 *
37914 * If the device is stationary, this returns 0,
37915 * otherwise it returns a non-negative number.
37916 * @type Number
37917 * @readonly
37918 */
37919 speed: null,
37920
37921 /**
37922 * Read-only property representing when the last retrieved
37923 * positioning information was acquired by the device.
37924 * @type Date
37925 * @readonly
37926 */
37927 timestamp: null,
37928
37929 //PositionOptions interface
37930 /**
37931 * @cfg {Boolean} allowHighAccuracy
37932 * When set to `true`, provide a hint that the application would like to receive
37933 * the best possible results. This may result in slower response times or increased power consumption.
37934 * The user might also deny this capability, or the device might not be able to provide more accurate
37935 * results than if this option was set to `false`.
37936 */
37937 allowHighAccuracy: false,
37938
37939 /**
37940 * @cfg {Number} timeout
37941 * The maximum number of milliseconds allowed to elapse between a location update operation
37942 * and the corresponding {@link #locationupdate} event being raised. If a location was not successfully
37943 * acquired before the given timeout elapses (and no other internal errors have occurred in this interval),
37944 * then a {@link #locationerror} event will be raised indicating a timeout as the cause.
37945 *
37946 * Note that the time that is spent obtaining the user permission is **not** included in the period
37947 * covered by the timeout. The `timeout` attribute only applies to the location acquisition operation.
37948 *
37949 * In the case of calling `updateLocation`, the {@link #locationerror} event will be raised only once.
37950 *
37951 * If {@link #autoUpdate} is set to `true`, the {@link #locationerror} event could be raised repeatedly.
37952 * The first timeout is relative to the moment {@link #autoUpdate} was set to `true`
37953 * (or this {@link Ext.util.Geolocation} was initialized with the {@link #autoUpdate} config option set to `true`).
37954 * Subsequent timeouts are relative to the moment when the device determines that it's position has changed.
37955 */
37956
37957 timeout: Infinity,
37958
37959 /**
37960 * @cfg {Number} maximumAge
37961 * This option indicates that the application is willing to accept cached location information whose age
37962 * is no greater than the specified time in milliseconds. If `maximumAge` is set to 0, an attempt to retrieve
37963 * new location information is made immediately.
37964 *
37965 * Setting the `maximumAge` to Infinity returns a cached position regardless of its age.
37966 *
37967 * If the device does not have cached location information available whose age is no
37968 * greater than the specified `maximumAge`, then it must acquire new location information.
37969 *
37970 * For example, if location information no older than 10 minutes is required, set this property to 600000.
37971 */
37972 maximumAge: 0,
37973
37974 // @private
37975 provider : undefined
37976 },
37977
37978 updateMaximumAge: function() {
37979 if (this.watchOperation) {
37980 this.updateWatchOperation();
37981 }
37982 },
37983
37984 updateTimeout: function() {
37985 if (this.watchOperation) {
37986 this.updateWatchOperation();
37987 }
37988 },
37989
37990 updateAllowHighAccuracy: function() {
37991 if (this.watchOperation) {
37992 this.updateWatchOperation();
37993 }
37994 },
37995
37996 applyProvider: function(config) {
37997 if (Ext.feature.has.Geolocation) {
37998 if (!config) {
37999 if (navigator && navigator.geolocation) {
38000 config = navigator.geolocation;
38001 }
38002 else if (window.google) {
38003 config = google.gears.factory.create('beta.geolocation');
38004 }
38005 }
38006 }
38007 else {
38008 this.fireEvent('locationerror', this, false, false, true, 'This device does not support Geolocation.');
38009 }
38010 return config;
38011 },
38012
38013 updateAutoUpdate: function(newAutoUpdate, oldAutoUpdate) {
38014 var me = this,
38015 provider = me.getProvider();
38016
38017 if (oldAutoUpdate && provider) {
38018 clearInterval(me.watchOperationId);
38019 me.watchOperationId = null;
38020 }
38021
38022 if (newAutoUpdate) {
38023 if (!provider) {
38024 me.fireEvent('locationerror', me, false, false, true, null);
38025 return;
38026 }
38027
38028 try {
38029 me.updateWatchOperation();
38030 }
38031 catch(e) {
38032 me.fireEvent('locationerror', me, false, false, true, e.message);
38033 }
38034 }
38035 },
38036
38037 // @private
38038 updateWatchOperation: function() {
38039 var me = this,
38040 provider = me.getProvider();
38041
38042 // The native watchPosition method is currently broken in iOS5...
38043
38044 if (me.watchOperationId) {
38045 clearInterval(me.watchOperationId);
38046 }
38047
38048 function pollPosition() {
38049 provider.getCurrentPosition(
38050 Ext.bind(me.fireUpdate, me),
38051 Ext.bind(me.fireError, me),
38052 me.parseOptions()
38053 );
38054 }
38055
38056 pollPosition();
38057 me.watchOperationId = setInterval(pollPosition, this.getFrequency());
38058 },
38059
38060 /**
38061 * Executes a onetime location update operation,
38062 * raising either a {@link #locationupdate} or {@link #locationerror} event.
38063 *
38064 * Does not interfere with or restart ongoing location monitoring.
38065 * @param {Function} callback
38066 * A callback method to be called when the location retrieval has been completed.
38067 *
38068 * Will be called on both success and failure.
38069 *
38070 * The method will be passed one parameter, {@link Ext.util.Geolocation}
38071 * (**this** reference), set to `null` on failure.
38072 *
38073 * geo.updateLocation(function (geo) {
38074 * alert('Latitude: ' + (geo !== null ? geo.latitude : 'failed'));
38075 * });
38076 *
38077 * @param {Object} [scope]
38078 * The scope (**this** reference) in which the handler function is executed.
38079 *
38080 * **If omitted, defaults to the object which fired the event.**
38081 *
38082 * <!--positonOptions undocumented param, see W3C spec-->
38083 */
38084 updateLocation: function(callback, scope, positionOptions) {
38085 var me = this,
38086 provider = me.getProvider();
38087
38088 var failFunction = function(message, error) {
38089 if (error) {
38090 me.fireError(error);
38091 }
38092 else {
38093 me.fireEvent('locationerror', me, false, false, true, message);
38094 }
38095 if (callback) {
38096 callback.call(scope || me, null, me); //last parameter for legacy purposes
38097 }
38098 };
38099
38100 if (!provider) {
38101 failFunction(null);
38102 return;
38103 }
38104
38105 try {
38106 provider.getCurrentPosition(
38107 //success callback
38108 function(position) {
38109 me.fireUpdate(position);
38110 if (callback) {
38111 callback.call(scope || me, me, me); //last parameter for legacy purposes
38112 }
38113 },
38114 //error callback
38115 function(error) {
38116 failFunction(null, error);
38117 },
38118 positionOptions || me.parseOptions()
38119 );
38120 }
38121 catch(e) {
38122 failFunction(e.message);
38123 }
38124 },
38125
38126 // @private
38127 fireUpdate: function(position) {
38128 var me = this,
38129 coords = position.coords;
38130
38131 this.position = position;
38132
38133 me.setConfig({
38134 timestamp: position.timestamp,
38135 latitude: coords.latitude,
38136 longitude: coords.longitude,
38137 accuracy: coords.accuracy,
38138 altitude: coords.altitude,
38139 altitudeAccuracy: coords.altitudeAccuracy,
38140 heading: coords.heading,
38141 speed: coords.speed
38142 });
38143
38144 me.fireEvent('locationupdate', me);
38145 },
38146
38147 // @private
38148 fireError: function(error) {
38149 var errorCode = error.code;
38150 this.fireEvent('locationerror', this,
38151 errorCode == error.TIMEOUT,
38152 errorCode == error.PERMISSION_DENIED,
38153 errorCode == error.POSITION_UNAVAILABLE,
38154 error.message == undefined ? null : error.message
38155 );
38156 },
38157
38158 // @private
38159 parseOptions: function() {
38160 var timeout = this.getTimeout(),
38161 ret = {
38162 maximumAge: this.getMaximumAge(),
38163 enableHighAccuracy: this.getAllowHighAccuracy()
38164 };
38165
38166 //Google doesn't like Infinity
38167 if (timeout !== Infinity) {
38168 ret.timeout = timeout;
38169 }
38170 return ret;
38171 },
38172
38173 destroy : function() {
38174 this.setAutoUpdate(false);
38175 }
38176 });
38177
38178 /**
38179 * Wraps a Google Map in an Ext.Component using the [Google Maps API](http://code.google.com/apis/maps/documentation/v3/introduction.html).
38180 *
38181 * To use this component you must include an additional JavaScript file from Google:
38182 *
38183 * <script type="text/javascript" src="http://maps.google.com/maps/api/js?sensor=true"></script>
38184 *
38185 * ## Example
38186 *
38187 * Ext.Viewport.add({
38188 * xtype: 'map',
38189 * useCurrentLocation: true
38190 * });
38191 *
38192 */
38193 Ext.define('Ext.Map', {
38194 extend: Ext.Container ,
38195 xtype : 'map',
38196
38197
38198 isMap: true,
38199
38200 config: {
38201 /**
38202 * @event maprender
38203 * Fired when Map initially rendered.
38204 * @param {Ext.Map} this
38205 * @param {google.maps.Map} map The rendered google.map.Map instance
38206 */
38207
38208 /**
38209 * @event centerchange
38210 * Fired when map is panned around.
38211 * @param {Ext.Map} this
38212 * @param {google.maps.Map} map The rendered google.map.Map instance
38213 * @param {google.maps.LatLng} center The current LatLng center of the map
38214 */
38215
38216 /**
38217 * @event typechange
38218 * Fired when display type of the map changes.
38219 * @param {Ext.Map} this
38220 * @param {google.maps.Map} map The rendered google.map.Map instance
38221 * @param {Number} mapType The current display type of the map
38222 */
38223
38224 /**
38225 * @event zoomchange
38226 * Fired when map is zoomed.
38227 * @param {Ext.Map} this
38228 * @param {google.maps.Map} map The rendered google.map.Map instance
38229 * @param {Number} zoomLevel The current zoom level of the map
38230 */
38231
38232 /**
38233 * @cfg {String} baseCls
38234 * The base CSS class to apply to the Map's element
38235 * @accessor
38236 */
38237 baseCls: Ext.baseCSSPrefix + 'map',
38238
38239 /**
38240 * @cfg {Boolean/Ext.util.Geolocation} useCurrentLocation
38241 * Pass in true to center the map based on the geolocation coordinates or pass a
38242 * {@link Ext.util.Geolocation GeoLocation} config to have more control over your GeoLocation options
38243 * @accessor
38244 */
38245 useCurrentLocation: false,
38246
38247 /**
38248 * @cfg {google.maps.Map} map
38249 * The wrapped map.
38250 * @accessor
38251 */
38252 map: null,
38253
38254 /**
38255 * @cfg {Ext.util.Geolocation} geo
38256 * Geolocation provider for the map.
38257 * @accessor
38258 */
38259 geo: null,
38260
38261 /**
38262 * @cfg {Object} mapOptions
38263 * MapOptions as specified by the Google Documentation:
38264 * [http://code.google.com/apis/maps/documentation/v3/reference.html](http://code.google.com/apis/maps/documentation/v3/reference.html)
38265 * @accessor
38266 */
38267 mapOptions: {},
38268
38269 /**
38270 * @cfg {Object} mapListeners
38271 * Listeners for any Google Maps events specified by the Google Documentation:
38272 * [http://code.google.com/apis/maps/documentation/v3/reference.html](http://code.google.com/apis/maps/documentation/v3/reference.html)
38273 *
38274 * @accessor
38275 */
38276 mapListeners: null
38277 },
38278
38279 constructor: function() {
38280 this.callParent(arguments);
38281 // this.element.setVisibilityMode(Ext.Element.OFFSETS);
38282
38283 if (!(window.google || {}).maps) {
38284 this.setHtml('Google Maps API is required');
38285 }
38286 },
38287
38288 initialize: function() {
38289 this.callParent();
38290 this.initMap();
38291
38292 this.on({
38293 painted: 'doResize',
38294 scope: this
38295 });
38296 this.innerElement.on('touchstart', 'onTouchStart', this);
38297 },
38298
38299 initMap: function() {
38300 var map = this.getMap();
38301 if(!map) {
38302 var gm = (window.google || {}).maps;
38303 if(!gm) return null;
38304
38305 var element = this.mapContainer,
38306 mapOptions = this.getMapOptions(),
38307 event = gm.event,
38308 me = this;
38309
38310 //Remove the API Required div
38311 if (element.dom.firstChild) {
38312 Ext.fly(element.dom.firstChild).destroy();
38313 }
38314
38315 if (Ext.os.is.iPad) {
38316 Ext.merge({
38317 navigationControlOptions: {
38318 style: gm.NavigationControlStyle.ZOOM_PAN
38319 }
38320 }, mapOptions);
38321 }
38322
38323 mapOptions.mapTypeId = mapOptions.mapTypeId || gm.MapTypeId.ROADMAP;
38324 mapOptions.center = mapOptions.center || new gm.LatLng(37.381592, -122.135672); // Palo Alto
38325
38326 if (mapOptions.center && mapOptions.center.latitude && !Ext.isFunction(mapOptions.center.lat)) {
38327 mapOptions.center = new gm.LatLng(mapOptions.center.latitude, mapOptions.center.longitude);
38328 }
38329
38330 mapOptions.zoom = mapOptions.zoom || 12;
38331
38332 map = new gm.Map(element.dom, mapOptions);
38333 this.setMap(map);
38334
38335 event.addListener(map, 'zoom_changed', Ext.bind(me.onZoomChange, me));
38336 event.addListener(map, 'maptypeid_changed', Ext.bind(me.onTypeChange, me));
38337 event.addListener(map, 'center_changed', Ext.bind(me.onCenterChange, me));
38338 event.addListenerOnce(map, 'tilesloaded', Ext.bind(me.onTilesLoaded, me));
38339 this.addMapListeners();
38340 }
38341 return this.getMap();
38342 },
38343
38344 // added for backwards compatibility for touch < 2.3
38345 renderMap: function() {
38346 this.initMap();
38347 },
38348
38349 getElementConfig: function() {
38350 return {
38351 reference: 'element',
38352 className: 'x-container',
38353 children: [{
38354 reference: 'innerElement',
38355 className: 'x-inner',
38356 children: [{
38357 reference: 'mapContainer',
38358 className: Ext.baseCSSPrefix + 'map-container'
38359 }]
38360 }]
38361 };
38362 },
38363
38364 onTouchStart: function(e) {
38365 e.makeUnpreventable();
38366 },
38367
38368 applyMapOptions: function(options) {
38369 return Ext.merge({}, this.options, options);
38370 },
38371
38372 updateMapOptions: function(newOptions) {
38373 var gm = (window.google || {}).maps,
38374 map = this.getMap();
38375
38376 if (gm && map) {
38377 map.setOptions(newOptions);
38378 }
38379 },
38380
38381 doMapCenter: function() {
38382 this.setMapCenter(this.getMapOptions().center);
38383 },
38384
38385 getMapOptions: function() {
38386 return Ext.merge({}, this.options || this.getInitialConfig('mapOptions'));
38387 },
38388
38389 updateUseCurrentLocation: function(useCurrentLocation) {
38390 this.setGeo(useCurrentLocation);
38391 if (!useCurrentLocation) {
38392 this.setMapCenter();
38393 }
38394 },
38395
38396 applyGeo: function(config) {
38397 return Ext.factory(config, Ext.util.Geolocation, this.getGeo());
38398 },
38399
38400 updateGeo: function(newGeo, oldGeo) {
38401 var events = {
38402 locationupdate : 'onGeoUpdate',
38403 locationerror : 'onGeoError',
38404 scope : this
38405 };
38406
38407 if (oldGeo) {
38408 oldGeo.un(events);
38409 }
38410
38411 if (newGeo) {
38412 newGeo.on(events);
38413 newGeo.updateLocation();
38414 }
38415 },
38416
38417 doResize: function() {
38418 var gm = (window.google || {}).maps,
38419 map = this.getMap();
38420
38421 if (gm && map) {
38422 gm.event.trigger(map, "resize");
38423 }
38424 },
38425
38426 // @private
38427 onTilesLoaded: function() {
38428 this.fireEvent('maprender', this, this.getMap());
38429 },
38430
38431 // @private
38432 addMapListeners: function() {
38433 var gm = (window.google || {}).maps,
38434 map = this.getMap(),
38435 mapListeners = this.getMapListeners();
38436
38437
38438 if (gm) {
38439 var event = gm.event,
38440 me = this,
38441 listener, scope, fn, callbackFn, handle;
38442 if (Ext.isSimpleObject(mapListeners)) {
38443 for (var eventType in mapListeners) {
38444 listener = mapListeners[eventType];
38445 if (Ext.isSimpleObject(listener)) {
38446 scope = listener.scope;
38447 fn = listener.fn;
38448 } else if (Ext.isFunction(listener)) {
38449 scope = null;
38450 fn = listener;
38451 }
38452
38453 if (fn) {
38454 callbackFn = function() {
38455 this.fn.apply(this.scope, [me]);
38456 if(this.handle) {
38457 event.removeListener(this.handle);
38458 delete this.handle;
38459 delete this.fn;
38460 delete this.scope;
38461 }
38462 };
38463 handle = event.addListener(map, eventType, Ext.bind(callbackFn, callbackFn));
38464 callbackFn.fn = fn;
38465 callbackFn.scope = scope;
38466 if(listener.single === true) callbackFn.handle = handle;
38467 }
38468 }
38469 }
38470 }
38471 },
38472
38473 // @private
38474 onGeoUpdate: function(geo) {
38475 if (geo) {
38476 this.setMapCenter(new google.maps.LatLng(geo.getLatitude(), geo.getLongitude()));
38477 }
38478 },
38479
38480 // @private
38481 onGeoError: Ext.emptyFn,
38482
38483 /**
38484 * Moves the map center to the designated coordinates hash of the form:
38485 *
38486 * { latitude: 37.381592, longitude: -122.135672 }
38487 *
38488 * or a google.maps.LatLng object representing to the target location.
38489 *
38490 * @param {Object/google.maps.LatLng} coordinates Object representing the desired Latitude and
38491 * longitude upon which to center the map.
38492 */
38493 setMapCenter: function(coordinates) {
38494 var me = this,
38495 map = me.getMap(),
38496 mapOptions = me.getMapOptions(),
38497 gm = (window.google || {}).maps;
38498 if (gm) {
38499 if (!coordinates) {
38500 if (map && map.getCenter) {
38501 coordinates = map.getCenter();
38502 }
38503 else if (mapOptions.hasOwnProperty('center')) {
38504 coordinates = mapOptions.center;
38505 }
38506 else {
38507 coordinates = new gm.LatLng(37.381592, -122.135672); // Palo Alto
38508 }
38509 }
38510
38511 if (coordinates && !(coordinates instanceof gm.LatLng) && 'longitude' in coordinates) {
38512 coordinates = new gm.LatLng(coordinates.latitude, coordinates.longitude);
38513 }
38514
38515 if (!map) {
38516 mapOptions.center = mapOptions.center || coordinates;
38517 me.renderMap();
38518 map = me.getMap();
38519 }
38520
38521 if (map && coordinates instanceof gm.LatLng) {
38522 map.panTo(coordinates);
38523 }
38524 else {
38525 this.options = Ext.apply(this.getMapOptions(), {
38526 center: coordinates
38527 });
38528 }
38529 }
38530 },
38531
38532 // @private
38533 onZoomChange : function() {
38534 var mapOptions = this.getMapOptions(),
38535 map = this.getMap(),
38536 zoom;
38537
38538 zoom = (map && map.getZoom) ? map.getZoom() : mapOptions.zoom || 10;
38539
38540 this.options = Ext.apply(mapOptions, {
38541 zoom: zoom
38542 });
38543
38544 this.fireEvent('zoomchange', this, map, zoom);
38545 },
38546
38547 // @private
38548 onTypeChange : function() {
38549 var mapOptions = this.getMapOptions(),
38550 map = this.getMap(),
38551 mapTypeId;
38552
38553 mapTypeId = (map && map.getMapTypeId) ? map.getMapTypeId() : mapOptions.mapTypeId;
38554
38555 this.options = Ext.apply(mapOptions, {
38556 mapTypeId: mapTypeId
38557 });
38558
38559 this.fireEvent('typechange', this, map, mapTypeId);
38560 },
38561
38562 // @private
38563 onCenterChange: function() {
38564 var mapOptions = this.getMapOptions(),
38565 map = this.getMap(),
38566 center;
38567
38568 center = (map && map.getCenter) ? map.getCenter() : mapOptions.center;
38569
38570 this.options = Ext.apply(mapOptions, {
38571 center: center
38572 });
38573
38574 this.fireEvent('centerchange', this, map, center);
38575
38576 },
38577
38578 // @private
38579 destroy: function() {
38580 Ext.destroy(this.getGeo());
38581 var map = this.getMap();
38582
38583 if (map && (window.google || {}).maps) {
38584 google.maps.event.clearInstanceListeners(map);
38585 }
38586
38587 this.callParent();
38588 }
38589 }, function() {
38590 });
38591
38592 Ext.define('Ext.BingMap', {
38593 extend: Ext.Map ,
38594 xtype : 'bingmap',
38595
38596
38597 // @private
38598 initMap: function() {
38599 var map = this.getMap();
38600 if(!map) {
38601 var me = this,
38602 element = me.mapContainer,
38603 mapOptions = me.getMapOptions(),
38604 event;
38605
38606 var MM = Microsoft.Maps;
38607 var key = "AokX-S2lieXTaXG8pvEw3i2AKYuStBMK8RsUu6BDJ6hrL5AYv0IfQqM9zc-BAA-v";
38608 //TODO Investigate why does merge lead to exception in Bing
38609 mapOptions = Ext.merge({
38610 credentials: key,
38611 mapTypeId: "r",
38612 zoom: 12
38613 }, mapOptions);
38614
38615 // This is done separately from the above merge so we don't have to instantiate
38616 // a new LatLng if we don't need to
38617 if (!mapOptions.center) {
38618 mapOptions.center = new MM.Location(37.381592, -122.135672); // Palo Alto
38619 }
38620
38621 if (element.dom.firstChild) {
38622 Ext.fly(element.dom.firstChild).destroy();
38623 }
38624
38625 MM.loadModule('Microsoft.Maps.Overlays.Style', { callback: function () {
38626 me.setMap(new MM.Map(element.dom,mapOptions));
38627 if(mapOptions.callback){
38628 mapOptions.callback();
38629 }
38630 }
38631 });
38632
38633 map = me.getMap();
38634 }
38635
38636 //Track zoomLevel and mapType changes
38637 // event = MM.event;
38638 //TODO Investigate how to add listeners in Bing
38639 // event.addListener(map, 'zoom_changed', Ext.bind(me.onZoomChange, me));
38640 // event.addListener(map, 'maptypeid_changed', Ext.bind(me.onTypeChange, me));
38641 // event.addListener(map, 'center_changed', Ext.bind(me.onCenterChange, me));
38642
38643 me.fireEvent('maprender', me, map);
38644 },
38645 setMapCenter: function(coordinates) {
38646 var me = this,
38647 map = me.getMap(),
38648 MM = Microsoft.Maps;
38649
38650 if (!me.isPainted()) {
38651 me.un('painted', 'setMapCenter', this);
38652 me.on('painted', 'setMapCenter', this, { delay: 150, single: true, args: [coordinates] });
38653 return;
38654 }
38655 coordinates = coordinates || new MM.Location(37.381592, -122.135672);
38656
38657 if (coordinates && !(coordinates instanceof MM.Location) && 'longitude' in coordinates) {
38658 coordinates = new MM.Location(coordinates.latitude, coordinates.longitude);
38659 }
38660
38661 if (!map) {
38662 me.initMap();
38663 map = me.getMap();
38664 }
38665
38666 if (map && coordinates instanceof MM.Location) {
38667 map.updateMapPosition(coordinates);
38668 }
38669 else {
38670 this.options = Ext.apply(this.getMapOptions(), {
38671 center: coordinates
38672 });
38673 }
38674 }
38675 }, function() {
38676
38677 });
38678
38679 /**
38680 * @class Ext.ComponentQuery
38681 * @extends Object
38682 * @singleton
38683 *
38684 * Provides searching of Components within {@link Ext.ComponentManager} (globally) or a specific
38685 * {@link Ext.Container} on the document with a similar syntax to a CSS selector.
38686 *
38687 * Components can be retrieved by using their {@link Ext.Component xtype} with an optional '.' prefix
38688 *
38689 * - `component` or `.component`
38690 * - `gridpanel` or `.gridpanel`
38691 *
38692 * An itemId or id must be prefixed with a #
38693 *
38694 * - `#myContainer`
38695 *
38696 * Attributes must be wrapped in brackets
38697 *
38698 * - `component[autoScroll]`
38699 * - `panel[title="Test"]`
38700 *
38701 * Attributes can use the '=' or '~=' operators to do the pattern matching.
38702 *
38703 * The <strong>'='</strong> operator will return the results that <strong>exactly</strong> match:
38704 *
38705 * Ext.Component.query('panel[cls=my-cls]')
38706 *
38707 * Will match the following Component:
38708 *
38709 * Ext.create('Ext.Panel', {
38710 * cls : 'my-cls'
38711 * });
38712 *
38713 * The <strong>'~='</strong> operator will return results that <strong>exactly</strong> matches one of the whitespace-separated values:
38714 *
38715 * Ext.Component.query('panel[cls~=my-cls]')
38716 *
38717 * Will match the follow Component:
38718 *
38719 * Ext.create('My.Panel', {
38720 * cls : 'foo-cls my-cls bar-cls'
38721 * });
38722 *
38723 * This is because it <strong>exactly</strong> matched the 'my-cls' within the cls config.
38724 *
38725 * Member expressions from candidate Components may be tested. If the expression returns a *truthy* value,
38726 * the candidate Component will be included in the query:
38727 *
38728 * var disabledFields = myFormPanel.query("{isDisabled()}");
38729 *
38730 * Pseudo classes may be used to filter results in the same way as in {@link Ext.DomQuery DomQuery}:
38731 *
38732 * // Function receives array and returns a filtered array.
38733 * Ext.ComponentQuery.pseudos.invalid = function(items) {
38734 * var i = 0, l = items.length, c, result = [];
38735 * for (; i < l; i++) {
38736 * if (!(c = items[i]).isValid()) {
38737 * result.push(c);
38738 * }
38739 * }
38740 * return result;
38741 * };
38742 *
38743 * var invalidFields = myFormPanel.query('field:invalid');
38744 * if (invalidFields.length) {
38745 * invalidFields[0].getEl().scrollIntoView(myFormPanel.body);
38746 * for (var i = 0, l = invalidFields.length; i < l; i++) {
38747 * invalidFields[i].getEl().frame("red");
38748 * }
38749 * }
38750 *
38751 * Default pseudos include:
38752 *
38753 * - not
38754 *
38755 * Queries return an array of components.
38756 * Here are some example queries.
38757 *
38758 * // retrieve all Ext.Panels in the document by xtype
38759 * var panelsArray = Ext.ComponentQuery.query('panel');
38760 *
38761 * // retrieve all Ext.Panels within the container with an id myCt
38762 * var panelsWithinmyCt = Ext.ComponentQuery.query('#myCt panel');
38763 *
38764 * // retrieve all direct children which are Ext.Panels within myCt
38765 * var directChildPanel = Ext.ComponentQuery.query('#myCt > panel');
38766 *
38767 * // retrieve all grids and trees
38768 * var gridsAndTrees = Ext.ComponentQuery.query('gridpanel, treepanel');
38769 *
38770 * For easy access to queries based from a particular Container see the {@link Ext.Container#query},
38771 * {@link Ext.Container#down} and {@link Ext.Container#child} methods. Also see
38772 * {@link Ext.Component#up}.
38773 */
38774 Ext.define('Ext.ComponentQuery', {
38775 singleton: true
38776
38777 }, function() {
38778
38779 var cq = this,
38780
38781 // A function source code pattern with a placeholder which accepts an expression which yields a truth value when applied
38782 // as a member on each item in the passed array.
38783 filterFnPattern = [
38784 'var r = [],',
38785 'i = 0,',
38786 'it = items,',
38787 'l = it.length,',
38788 'c;',
38789 'for (; i < l; i++) {',
38790 'c = it[i];',
38791 'if (c.{0}) {',
38792 'r.push(c);',
38793 '}',
38794 '}',
38795 'return r;'
38796 ].join(''),
38797
38798 filterItems = function(items, operation) {
38799 // Argument list for the operation is [ itemsArray, operationArg1, operationArg2...]
38800 // The operation's method loops over each item in the candidate array and
38801 // returns an array of items which match its criteria
38802 return operation.method.apply(this, [ items ].concat(operation.args));
38803 },
38804
38805 getItems = function(items, mode) {
38806 var result = [],
38807 i = 0,
38808 length = items.length,
38809 candidate,
38810 deep = mode !== '>';
38811
38812 for (; i < length; i++) {
38813 candidate = items[i];
38814 if (candidate.getRefItems) {
38815 result = result.concat(candidate.getRefItems(deep));
38816 }
38817 }
38818 return result;
38819 },
38820
38821 getAncestors = function(items) {
38822 var result = [],
38823 i = 0,
38824 length = items.length,
38825 candidate;
38826 for (; i < length; i++) {
38827 candidate = items[i];
38828 while (!!(candidate = (candidate.ownerCt || candidate.floatParent))) {
38829 result.push(candidate);
38830 }
38831 }
38832 return result;
38833 },
38834
38835 // Filters the passed candidate array and returns only items which match the passed xtype
38836 filterByXType = function(items, xtype, shallow) {
38837 if (xtype === '*') {
38838 return items.slice();
38839 }
38840 else {
38841 var result = [],
38842 i = 0,
38843 length = items.length,
38844 candidate;
38845 for (; i < length; i++) {
38846 candidate = items[i];
38847 if (candidate.isXType(xtype, shallow)) {
38848 result.push(candidate);
38849 }
38850 }
38851 return result;
38852 }
38853 },
38854
38855 // Filters the passed candidate array and returns only items which have the passed className
38856 filterByClassName = function(items, className) {
38857 var EA = Ext.Array,
38858 result = [],
38859 i = 0,
38860 length = items.length,
38861 candidate;
38862 for (; i < length; i++) {
38863 candidate = items[i];
38864 if (candidate.el ? candidate.el.hasCls(className) : EA.contains(candidate.initCls(), className)) {
38865 result.push(candidate);
38866 }
38867 }
38868 return result;
38869 },
38870
38871 // Filters the passed candidate array and returns only items which have the specified property match
38872 filterByAttribute = function(items, property, operator, value) {
38873 var result = [],
38874 i = 0,
38875 length = items.length,
38876 candidate, getter, getValue;
38877 for (; i < length; i++) {
38878 candidate = items[i];
38879 getter = Ext.Class.getConfigNameMap(property).get;
38880 if (operator === '~=') {
38881 getValue = null;
38882
38883 if (candidate[getter]) {
38884 getValue = candidate[getter]();
38885 } else if (candidate.config && candidate.config[property]) {
38886 getValue = String(candidate.config[property]);
38887 } else if (candidate[property]) {
38888 getValue = String(candidate[property]);
38889 }
38890
38891 if (getValue) {
38892 //normalize to an array
38893 if (!Ext.isArray(getValue)) {
38894 getValue = getValue.split(' ');
38895 }
38896
38897 var v = 0,
38898 vLen = getValue.length,
38899 val;
38900
38901 for (; v < vLen; v++) {
38902 /**
38903 * getValue[v] could still be whitespaced-separated, this normalizes it. This is an example:
38904 *
38905 * {
38906 * html : 'Imprint',
38907 * cls : 'overlay-footer-item overlay-footer-imprint'
38908 * }
38909 */
38910 val = String(getValue[v]).split(' ');
38911
38912 if (Ext.Array.indexOf(val, value) !== -1) {
38913 result.push(candidate);
38914 }
38915 }
38916 }
38917 } else if (candidate[getter]) {
38918 getValue = candidate[getter]();
38919 if (!value ? !!getValue : (String(getValue) === value)) {
38920 result.push(candidate);
38921 }
38922 }
38923 else if (candidate.config && candidate.config[property]) {
38924 if (!value ? !!candidate.config[property] : (String(candidate.config[property]) === value)) {
38925 result.push(candidate);
38926 }
38927 }
38928 else if (!value ? !!candidate[property] : (String(candidate[property]) === value)) {
38929 result.push(candidate);
38930 }
38931 }
38932 return result;
38933 },
38934
38935 // Filters the passed candidate array and returns only items which have the specified itemId or id
38936 filterById = function(items, id) {
38937 var result = [],
38938 i = 0,
38939 length = items.length,
38940 candidate;
38941 for (; i < length; i++) {
38942 candidate = items[i];
38943 if (candidate.getId() === id || candidate.getItemId() === id) {
38944 result.push(candidate);
38945 }
38946 }
38947 return result;
38948 },
38949
38950 // Filters the passed candidate array and returns only items which the named pseudo class matcher filters in
38951 filterByPseudo = function(items, name, value) {
38952 return cq.pseudos[name](items, value);
38953 },
38954
38955 // Determines leading mode
38956 // > for direct child, and ^ to switch to ownerCt axis
38957 modeRe = /^(\s?([>\^])\s?|\s|$)/,
38958
38959 // Matches a token with possibly (true|false) appended for the "shallow" parameter
38960 tokenRe = /^(#)?([\w\-]+|\*)(?:\((true|false)\))?/,
38961
38962 matchers = [{
38963 // Checks for .xtype with possibly (true|false) appended for the "shallow" parameter
38964 re: /^\.([\w\-]+)(?:\((true|false)\))?/,
38965 method: filterByXType
38966 },{
38967 // checks for [attribute=value]
38968 re: /^(?:[\[](?:@)?([\w\-]+)\s?(?:(=|.=)\s?['"]?(.*?)["']?)?[\]])/,
38969 method: filterByAttribute
38970 }, {
38971 // checks for #cmpItemId
38972 re: /^#([\w\-]+)/,
38973 method: filterById
38974 }, {
38975 // checks for :<pseudo_class>(<selector>)
38976 re: /^\:([\w\-]+)(?:\(((?:\{[^\}]+\})|(?:(?!\{)[^\s>\/]*?(?!\})))\))?/,
38977 method: filterByPseudo
38978 }, {
38979 // checks for {<member_expression>}
38980 re: /^(?:\{([^\}]+)\})/,
38981 method: filterFnPattern
38982 }];
38983
38984 cq.Query = Ext.extend(Object, {
38985 constructor: function(cfg) {
38986 cfg = cfg || {};
38987 Ext.apply(this, cfg);
38988 },
38989
38990 /**
38991 * @private
38992 * Executes this Query upon the selected root.
38993 * The root provides the initial source of candidate Component matches which are progressively
38994 * filtered by iterating through this Query's operations cache.
38995 * If no root is provided, all registered Components are searched via the ComponentManager.
38996 * root may be a Container who's descendant Components are filtered
38997 * root may be a Component with an implementation of getRefItems which provides some nested Components such as the
38998 * docked items within a Panel.
38999 * root may be an array of candidate Components to filter using this Query.
39000 */
39001 execute : function(root) {
39002 var operations = this.operations,
39003 i = 0,
39004 length = operations.length,
39005 operation,
39006 workingItems;
39007
39008 // no root, use all Components in the document
39009 if (!root) {
39010 workingItems = Ext.ComponentManager.all.getArray();
39011 }
39012 // Root is a candidate Array
39013 else if (Ext.isArray(root)) {
39014 workingItems = root;
39015 }
39016
39017 // We are going to loop over our operations and take care of them
39018 // one by one.
39019 for (; i < length; i++) {
39020 operation = operations[i];
39021
39022 // The mode operation requires some custom handling.
39023 // All other operations essentially filter down our current
39024 // working items, while mode replaces our current working
39025 // items by getting children from each one of our current
39026 // working items. The type of mode determines the type of
39027 // children we get. (e.g. > only gets direct children)
39028 if (operation.mode === '^') {
39029 workingItems = getAncestors(workingItems || [root]);
39030 }
39031 else if (operation.mode) {
39032 workingItems = getItems(workingItems || [root], operation.mode);
39033 }
39034 else {
39035 workingItems = filterItems(workingItems || getItems([root]), operation);
39036 }
39037
39038 // If this is the last operation, it means our current working
39039 // items are the final matched items. Thus return them!
39040 if (i === length -1) {
39041 return workingItems;
39042 }
39043 }
39044 return [];
39045 },
39046
39047 is: function(component) {
39048 var operations = this.operations,
39049 components = Ext.isArray(component) ? component : [component],
39050 originalLength = components.length,
39051 lastOperation = operations[operations.length-1],
39052 ln, i;
39053
39054 components = filterItems(components, lastOperation);
39055 if (components.length === originalLength) {
39056 if (operations.length > 1) {
39057 for (i = 0, ln = components.length; i < ln; i++) {
39058 if (Ext.Array.indexOf(this.execute(), components[i]) === -1) {
39059 return false;
39060 }
39061 }
39062 }
39063 return true;
39064 }
39065 return false;
39066 }
39067 });
39068
39069 Ext.apply(this, {
39070
39071 // private cache of selectors and matching ComponentQuery.Query objects
39072 cache: {},
39073
39074 // private cache of pseudo class filter functions
39075 pseudos: {
39076 not: function(components, selector){
39077 var CQ = Ext.ComponentQuery,
39078 i = 0,
39079 length = components.length,
39080 results = [],
39081 index = -1,
39082 component;
39083
39084 for(; i < length; ++i) {
39085 component = components[i];
39086 if (!CQ.is(component, selector)) {
39087 results[++index] = component;
39088 }
39089 }
39090 return results;
39091 }
39092 },
39093
39094 /**
39095 * Returns an array of matched Components from within the passed root object.
39096 *
39097 * This method filters returned Components in a similar way to how CSS selector based DOM
39098 * queries work using a textual selector string.
39099 *
39100 * See class summary for details.
39101 *
39102 * @param {String} selector The selector string to filter returned Components
39103 * @param {Ext.Container} root The Container within which to perform the query.
39104 * If omitted, all Components within the document are included in the search.
39105 *
39106 * This parameter may also be an array of Components to filter according to the selector.
39107 * @return {Ext.Component[]} The matched Components.
39108 *
39109 * @member Ext.ComponentQuery
39110 */
39111 query: function(selector, root) {
39112 var selectors = selector.split(','),
39113 length = selectors.length,
39114 i = 0,
39115 results = [],
39116 noDupResults = [],
39117 dupMatcher = {},
39118 query, resultsLn, cmp;
39119
39120 for (; i < length; i++) {
39121 selector = Ext.String.trim(selectors[i]);
39122 query = this.parse(selector);
39123 // query = this.cache[selector];
39124 // if (!query) {
39125 // this.cache[selector] = query = this.parse(selector);
39126 // }
39127 results = results.concat(query.execute(root));
39128 }
39129
39130 // multiple selectors, potential to find duplicates
39131 // lets filter them out.
39132 if (length > 1) {
39133 resultsLn = results.length;
39134 for (i = 0; i < resultsLn; i++) {
39135 cmp = results[i];
39136 if (!dupMatcher[cmp.id]) {
39137 noDupResults.push(cmp);
39138 dupMatcher[cmp.id] = true;
39139 }
39140 }
39141 results = noDupResults;
39142 }
39143 return results;
39144 },
39145
39146 /**
39147 * Tests whether the passed Component matches the selector string.
39148 * @param {Ext.Component} component The Component to test.
39149 * @param {String} selector The selector string to test against.
39150 * @return {Boolean} `true` if the Component matches the selector.
39151 * @member Ext.ComponentQuery
39152 */
39153 is: function(component, selector) {
39154 if (!selector) {
39155 return true;
39156 }
39157 var query = this.cache[selector];
39158 if (!query) {
39159 this.cache[selector] = query = this.parse(selector);
39160 }
39161 return query.is(component);
39162 },
39163
39164 parse: function(selector) {
39165 var operations = [],
39166 length = matchers.length,
39167 lastSelector,
39168 tokenMatch,
39169 matchedChar,
39170 modeMatch,
39171 selectorMatch,
39172 i, matcher, method;
39173
39174 // We are going to parse the beginning of the selector over and
39175 // over again, slicing off the selector any portions we converted into an
39176 // operation, until it is an empty string.
39177 while (selector && lastSelector !== selector) {
39178 lastSelector = selector;
39179
39180 // First we check if we are dealing with a token like #, * or an xtype
39181 tokenMatch = selector.match(tokenRe);
39182
39183 if (tokenMatch) {
39184 matchedChar = tokenMatch[1];
39185
39186 // If the token is prefixed with a # we push a filterById operation to our stack
39187 if (matchedChar === '#') {
39188 operations.push({
39189 method: filterById,
39190 args: [Ext.String.trim(tokenMatch[2])]
39191 });
39192 }
39193 // If the token is prefixed with a . we push a filterByClassName operation to our stack
39194 // FIXME: Not enabled yet. just needs \. adding to the tokenRe prefix
39195 else if (matchedChar === '.') {
39196 operations.push({
39197 method: filterByClassName,
39198 args: [Ext.String.trim(tokenMatch[2])]
39199 });
39200 }
39201 // If the token is a * or an xtype string, we push a filterByXType
39202 // operation to the stack.
39203 else {
39204 operations.push({
39205 method: filterByXType,
39206 args: [Ext.String.trim(tokenMatch[2]), Boolean(tokenMatch[3])]
39207 });
39208 }
39209
39210 // Now we slice of the part we just converted into an operation
39211 selector = selector.replace(tokenMatch[0], '');
39212 }
39213
39214 // If the next part of the query is not a space or > or ^, it means we
39215 // are going to check for more things that our current selection
39216 // has to comply to.
39217 while (!(modeMatch = selector.match(modeRe))) {
39218 // Lets loop over each type of matcher and execute it
39219 // on our current selector.
39220 for (i = 0; selector && i < length; i++) {
39221 matcher = matchers[i];
39222 selectorMatch = selector.match(matcher.re);
39223 method = matcher.method;
39224
39225 // If we have a match, add an operation with the method
39226 // associated with this matcher, and pass the regular
39227 // expression matches are arguments to the operation.
39228 if (selectorMatch) {
39229 operations.push({
39230 method: Ext.isString(matcher.method)
39231 // Turn a string method into a function by formatting the string with our selector matche expression
39232 // A new method is created for different match expressions, eg {id=='textfield-1024'}
39233 // Every expression may be different in different selectors.
39234 ? Ext.functionFactory('items', Ext.String.format.apply(Ext.String, [method].concat(selectorMatch.slice(1))))
39235 : matcher.method,
39236 args: selectorMatch.slice(1)
39237 });
39238 selector = selector.replace(selectorMatch[0], '');
39239 break; // Break on match
39240 }
39241 //<debug>
39242 // Exhausted all matches: It's an error
39243 if (i === (length - 1)) {
39244 Ext.Error.raise('Invalid ComponentQuery selector: "' + arguments[0] + '"');
39245 }
39246 //</debug>
39247 }
39248 }
39249
39250 // Now we are going to check for a mode change. This means a space
39251 // or a > to determine if we are going to select all the children
39252 // of the currently matched items, or a ^ if we are going to use the
39253 // ownerCt axis as the candidate source.
39254 if (modeMatch[1]) { // Assignment, and test for truthiness!
39255 operations.push({
39256 mode: modeMatch[2]||modeMatch[1]
39257 });
39258 selector = selector.replace(modeMatch[0], '');
39259 }
39260 }
39261
39262 // Now that we have all our operations in an array, we are going
39263 // to create a new Query using these operations.
39264 return new cq.Query({
39265 operations: operations
39266 });
39267 }
39268 });
39269 });
39270
39271 /**
39272 * @class Ext.Decorator
39273 * @extends Ext.Component
39274 *
39275 * In a few words, a Decorator is a Component that wraps around another Component. A typical example of a Decorator is a
39276 * {@link Ext.field.Field Field}. A form field is nothing more than a decorator around another component, and gives the
39277 * component a label, as well as extra styling to make it look good in a form.
39278 *
39279 * A Decorator can be thought of as a lightweight Container that has only one child item, and no layout overhead.
39280 * The look and feel of decorators can be styled purely in CSS.
39281 *
39282 * Another powerful feature that Decorator provides is config proxying. For example: all config items of a
39283 * {@link Ext.slider.Slider Slider} also exist in a {@link Ext.field.Slider Slider Field} for API convenience.
39284 * The {@link Ext.field.Slider Slider Field} simply proxies all corresponding getters and setters
39285 * to the actual {@link Ext.slider.Slider Slider} instance. Writing out all the setters and getters to do that is a tedious task
39286 * and a waste of code space. Instead, when you sub-class Ext.Decorator, all you need to do is to specify those config items
39287 * that you want to proxy to the Component using a special 'proxyConfig' class property. Here's how it may look like
39288 * in a Slider Field class:
39289 *
39290 * Ext.define('My.field.Slider', {
39291 * extend: 'Ext.Decorator',
39292 *
39293 * config: {
39294 * component: {
39295 * xtype: 'slider'
39296 * }
39297 * },
39298 *
39299 * proxyConfig: {
39300 * minValue: 0,
39301 * maxValue: 100,
39302 * increment: 1
39303 * }
39304 *
39305 * // ...
39306 * });
39307 *
39308 * Once `My.field.Slider` class is created, it will have all setters and getters methods for all items listed in `proxyConfig`
39309 * automatically generated. These methods all proxy to the same method names that exist within the Component instance.
39310 */
39311 Ext.define('Ext.Decorator', {
39312 extend: Ext.Component ,
39313
39314 isDecorator: true,
39315
39316 config: {
39317 /**
39318 * @cfg {Object} component The config object to factory the Component that this Decorator wraps around
39319 */
39320 component: {}
39321 },
39322
39323 statics: {
39324 generateProxySetter: function(name) {
39325 return function(value) {
39326 var component = this.getComponent();
39327 component[name].call(component, value);
39328
39329 return this;
39330 }
39331 },
39332 generateProxyGetter: function(name) {
39333 return function() {
39334 var component = this.getComponent();
39335 return component[name].call(component);
39336 }
39337 }
39338 },
39339
39340 onClassExtended: function(Class, members) {
39341 if (!members.hasOwnProperty('proxyConfig')) {
39342 return;
39343 }
39344
39345 var ExtClass = Ext.Class,
39346 proxyConfig = members.proxyConfig,
39347 config = members.config;
39348
39349 members.config = (config) ? Ext.applyIf(config, proxyConfig) : proxyConfig;
39350
39351 var name, nameMap, setName, getName;
39352
39353 for (name in proxyConfig) {
39354 if (proxyConfig.hasOwnProperty(name)) {
39355 nameMap = ExtClass.getConfigNameMap(name);
39356 setName = nameMap.set;
39357 getName = nameMap.get;
39358
39359 members[setName] = this.generateProxySetter(setName);
39360 members[getName] = this.generateProxyGetter(getName);
39361 }
39362 }
39363 },
39364
39365 // @private
39366 applyComponent: function(config) {
39367 return Ext.factory(config, Ext.Component);
39368 },
39369
39370 // @private
39371 updateComponent: function(newComponent, oldComponent) {
39372 if (oldComponent) {
39373 if (this.isRendered() && oldComponent.setRendered(false)) {
39374 oldComponent.fireAction('renderedchange', [this, oldComponent, false],
39375 'doUnsetComponent', this, { args: [oldComponent] });
39376 }
39377 else {
39378 this.doUnsetComponent(oldComponent);
39379 }
39380 }
39381
39382 if (newComponent) {
39383 if (this.isRendered() && newComponent.setRendered(true)) {
39384 newComponent.fireAction('renderedchange', [this, newComponent, true],
39385 'doSetComponent', this, { args: [newComponent] });
39386 }
39387 else {
39388 this.doSetComponent(newComponent);
39389 }
39390 }
39391 },
39392
39393 // @private
39394 doUnsetComponent: function(component) {
39395 if (component.renderElement.dom) {
39396 component.setLayoutSizeFlags(0);
39397 this.innerElement.dom.removeChild(component.renderElement.dom);
39398 }
39399 },
39400
39401 // @private
39402 doSetComponent: function(component) {
39403 if (component.renderElement.dom) {
39404 component.setLayoutSizeFlags(this.getSizeFlags());
39405 this.innerElement.dom.appendChild(component.renderElement.dom);
39406 }
39407 },
39408
39409 // @private
39410 setRendered: function(rendered) {
39411 var component;
39412
39413 if (this.callParent(arguments)) {
39414 component = this.getComponent();
39415
39416 if (component) {
39417 component.setRendered(rendered);
39418 }
39419
39420 return true;
39421 }
39422
39423 return false;
39424 },
39425
39426 // @private
39427 setDisabled: function(disabled) {
39428 this.callParent(arguments);
39429 this.getComponent().setDisabled(disabled);
39430 },
39431
39432 destroy: function() {
39433 Ext.destroy(this.getComponent());
39434 this.callParent();
39435 }
39436 });
39437
39438 /**
39439 * This is a simple way to add an image of any size to your application and have it participate in the layout system
39440 * like any other component. This component typically takes between 1 and 3 configurations - a {@link #src}, and
39441 * optionally a {@link #height} and a {@link #width}:
39442 *
39443 * @example miniphone
39444 * var img = Ext.create('Ext.Img', {
39445 * src: 'http://www.sencha.com/assets/images/sencha-avatar-64x64.png',
39446 * height: 64,
39447 * width: 64
39448 * });
39449 * Ext.Viewport.add(img);
39450 *
39451 * It's also easy to add an image into a panel or other container using its xtype:
39452 *
39453 * @example miniphone
39454 * Ext.create('Ext.Panel', {
39455 * fullscreen: true,
39456 * layout: 'hbox',
39457 * items: [
39458 * {
39459 * xtype: 'image',
39460 * src: 'http://www.sencha.com/assets/images/sencha-avatar-64x64.png',
39461 * flex: 1
39462 * },
39463 * {
39464 * xtype: 'panel',
39465 * flex: 2,
39466 * html: 'Sencha Inc.<br/>1700 Seaport Boulevard Suite 120, Redwood City, CA'
39467 * }
39468 * ]
39469 * });
39470 *
39471 * Here we created a panel which contains an image (a profile picture in this case) and a text area to allow the user
39472 * to enter profile information about themselves. In this case we used an {@link Ext.layout.HBox hbox layout} and
39473 * flexed the image to take up one third of the width and the text area to take two thirds of the width. See the
39474 * {@link Ext.layout.HBox hbox docs} for more information on flexing items.
39475 */
39476 Ext.define('Ext.Img', {
39477 extend: Ext.Component ,
39478 xtype: ['image', 'img'],
39479
39480 /**
39481 * @event tap
39482 * Fires whenever the component is tapped
39483 * @param {Ext.Img} this The Image instance
39484 * @param {Ext.EventObject} e The event object
39485 */
39486
39487 /**
39488 * @event load
39489 * Fires when the image is loaded
39490 * @param {Ext.Img} this The Image instance
39491 * @param {Ext.EventObject} e The event object
39492 */
39493
39494 /**
39495 * @event error
39496 * Fires if an error occured when trying to load the image
39497 * @param {Ext.Img} this The Image instance
39498 * @param {Ext.EventObject} e The event object
39499 */
39500
39501 config: {
39502 /**
39503 * @cfg {String} src The source of this image
39504 * @accessor
39505 */
39506 src: null,
39507
39508 /**
39509 * @cfg
39510 * @inheritdoc
39511 */
39512 baseCls : Ext.baseCSSPrefix + 'img',
39513
39514 /**
39515 * @cfg {String} imageCls The CSS class to be used when {@link #mode} is not set to 'background'
39516 * @accessor
39517 */
39518 imageCls : Ext.baseCSSPrefix + 'img-image',
39519
39520 /**
39521 * @cfg {String} backgroundCls The CSS class to be used when {@link #mode} is set to 'background'
39522 * @accessor
39523 */
39524 backgroundCls : Ext.baseCSSPrefix + 'img-background',
39525
39526 /**
39527 * @cfg {String} mode If set to 'background', uses a background-image CSS property instead of an
39528 * `<img>` tag to display the image.
39529 */
39530 mode: 'background'
39531 },
39532
39533 beforeInitialize: function() {
39534 var me = this;
39535 me.onLoad = Ext.Function.bind(me.onLoad, me);
39536 me.onError = Ext.Function.bind(me.onError, me);
39537 },
39538
39539 initialize: function() {
39540 var me = this;
39541 me.callParent();
39542
39543 me.relayEvents(me.renderElement, '*');
39544
39545 me.element.on({
39546 tap: 'onTap',
39547 scope: me
39548 });
39549 },
39550
39551 hide: function() {
39552 this.callParent(arguments);
39553 this.hiddenSrc = this.hiddenSrc || this.getSrc();
39554 this.setSrc(null);
39555 },
39556
39557 show: function() {
39558 this.callParent(arguments);
39559 if (this.hiddenSrc) {
39560 this.setSrc(this.hiddenSrc);
39561 delete this.hiddenSrc;
39562 }
39563 },
39564
39565 updateMode: function(mode) {
39566 var me = this,
39567 imageCls = me.getImageCls(),
39568 backgroundCls = me.getBackgroundCls();
39569
39570 if (mode === 'background') {
39571 if (me.imageElement) {
39572 me.imageElement.destroy();
39573 delete me.imageElement;
39574 me.updateSrc(me.getSrc());
39575 }
39576
39577 me.replaceCls(imageCls, backgroundCls);
39578 } else {
39579 me.imageElement = me.element.createChild({ tag: 'img' });
39580
39581 me.replaceCls(backgroundCls, imageCls);
39582 }
39583 },
39584
39585 updateImageCls : function (newCls, oldCls) {
39586 this.replaceCls(oldCls, newCls);
39587 },
39588
39589 updateBackgroundCls : function (newCls, oldCls) {
39590 this.replaceCls(oldCls, newCls);
39591 },
39592
39593 onTap: function(e) {
39594 this.fireEvent('tap', this, e);
39595 },
39596
39597 onAfterRender: function() {
39598 this.updateSrc(this.getSrc());
39599 },
39600
39601 /**
39602 * @private
39603 */
39604 updateSrc: function(newSrc) {
39605 var me = this,
39606 dom;
39607
39608 if (me.getMode() === 'background') {
39609 dom = this.imageObject || new Image();
39610 }
39611 else {
39612 dom = me.imageElement.dom;
39613 }
39614
39615 this.imageObject = dom;
39616
39617 dom.setAttribute('src', Ext.isString(newSrc) ? newSrc : '');
39618 dom.addEventListener('load', me.onLoad, false);
39619 dom.addEventListener('error', me.onError, false);
39620 },
39621
39622 detachListeners: function() {
39623 var dom = this.imageObject;
39624
39625 if (dom) {
39626 dom.removeEventListener('load', this.onLoad, false);
39627 dom.removeEventListener('error', this.onError, false);
39628 }
39629 },
39630
39631 onLoad : function(e) {
39632 this.detachListeners();
39633
39634 if (this.getMode() === 'background') {
39635 this.element.dom.style.backgroundImage = 'url("' + this.imageObject.src + '")';
39636 }
39637
39638 this.fireEvent('load', this, e);
39639 },
39640
39641 onError : function(e) {
39642 this.detachListeners();
39643
39644 // Attempt to set the src even though the error event fired.
39645 if (this.getMode() === 'background') {
39646 this.element.dom.style.backgroundImage = 'url("' + this.imageObject.src + '")';
39647 }
39648
39649 this.fireEvent('error', this, e);
39650 },
39651
39652 doSetWidth: function(width) {
39653 var sizingElement = (this.getMode() === 'background') ? this.element : this.imageElement;
39654
39655 sizingElement.setWidth(width);
39656
39657 this.callParent(arguments);
39658 },
39659
39660 doSetHeight: function(height) {
39661 var sizingElement = (this.getMode() === 'background') ? this.element : this.imageElement;
39662
39663 sizingElement.setHeight(height);
39664
39665 this.callParent(arguments);
39666 },
39667
39668 destroy: function() {
39669 this.detachListeners();
39670
39671 Ext.destroy(this.imageObject, this.imageElement);
39672 delete this.imageObject;
39673 delete this.imageElement;
39674
39675 this.callParent();
39676 }
39677 });
39678
39679 /**
39680 * A simple label component which allows you to insert content using {@link #html} configuration.
39681 *
39682 * @example miniphone
39683 * Ext.Viewport.add({
39684 * xtype: 'label',
39685 * html: 'My label!'
39686 * });
39687 */
39688 Ext.define('Ext.Label', {
39689 extend: Ext.Component ,
39690 xtype: 'label',
39691
39692 config: {
39693 baseCls: Ext.baseCSSPrefix + 'label'
39694
39695 /**
39696 * @cfg {String} html
39697 * The label of this component.
39698 */
39699 }
39700 });
39701
39702 /**
39703 * A simple class used to mask any {@link Ext.Container}.
39704 *
39705 * This should rarely be used directly, instead look at the {@link Ext.Container#masked} configuration.
39706 *
39707 * ## Example
39708 *
39709 * @example miniphone
39710 * Ext.Viewport.add({
39711 * masked: {
39712 * xtype: 'loadmask'
39713 * }
39714 * });
39715 *
39716 * You can customize the loading {@link #message} and whether or not you want to show the {@link #indicator}:
39717 *
39718 * @example miniphone
39719 * Ext.Viewport.add({
39720 * masked: {
39721 * xtype: 'loadmask',
39722 * message: 'A message..',
39723 * indicator: false
39724 * }
39725 * });
39726 *
39727 */
39728 Ext.define('Ext.LoadMask', {
39729 extend: Ext.Mask ,
39730 xtype: 'loadmask',
39731
39732 config: {
39733 /**
39734 * @cfg {String} message
39735 * The text to display in a centered loading message box.
39736 * @accessor
39737 */
39738 message: 'Loading...',
39739
39740 /**
39741 * @cfg {String} cls
39742 * The CSS Class for this component
39743 * @accessor
39744 */
39745 cls: Ext.baseCSSPrefix + 'loading-mask',
39746
39747 /**
39748 * @cfg {String} messageCls
39749 * The CSS class to apply to the loading message element.
39750 * @accessor
39751 */
39752 messageCls: Ext.baseCSSPrefix + 'mask-message',
39753
39754 /**
39755 * @cfg {Boolean} indicator
39756 * True to show the loading indicator on this {@link Ext.LoadMask}.
39757 * @accessor
39758 */
39759 indicator: true
39760 },
39761
39762 getTemplate: function() {
39763 var prefix = Ext.baseCSSPrefix;
39764
39765 return [
39766 {
39767 //it needs an inner so it can be centered within the mask, and have a background
39768 reference: 'innerElement',
39769 cls: prefix + 'mask-inner',
39770 children: [
39771 //the elements required for the CSS loading {@link #indicator}
39772 {
39773 reference: 'indicatorElement',
39774 cls: prefix + 'loading-spinner-outer',
39775 children: [
39776 {
39777 cls: prefix + 'loading-spinner',
39778 children: [
39779 { tag: 'span', cls: prefix + 'loading-top' },
39780 { tag: 'span', cls: prefix + 'loading-right' },
39781 { tag: 'span', cls: prefix + 'loading-bottom' },
39782 { tag: 'span', cls: prefix + 'loading-left' }
39783 ]
39784 }
39785 ]
39786 },
39787 //the element used to display the {@link #message}
39788 {
39789 reference: 'messageElement'
39790 }
39791 ]
39792 }
39793 ];
39794 },
39795
39796 /**
39797 * Updates the message element with the new value of the {@link #message} configuration
39798 * @private
39799 */
39800 updateMessage: function(newMessage) {
39801 var cls = Ext.baseCSSPrefix + 'has-message';
39802
39803 if (newMessage) {
39804 this.addCls(cls);
39805 } else {
39806 this.removeCls(cls);
39807 }
39808
39809 this.messageElement.setHtml(newMessage);
39810 },
39811
39812 /**
39813 * Replaces the cls of the message element with the value of the {@link #messageCls} configuration.
39814 * @private
39815 */
39816 updateMessageCls: function(newMessageCls, oldMessageCls) {
39817 this.messageElement.replaceCls(oldMessageCls, newMessageCls);
39818 },
39819
39820 /**
39821 * Shows or hides the loading indicator when the {@link #indicator} configuration is changed.
39822 * @private
39823 */
39824 updateIndicator: function(newIndicator) {
39825 this[newIndicator ? 'removeCls' : 'addCls'](Ext.baseCSSPrefix + 'indicator-hidden');
39826 }
39827
39828 }, function() {
39829 });
39830
39831 /**
39832 * {@link Ext.Menu}'s are used with {@link Ext.Viewport#setMenu}. A menu can be linked with any side of the screen (top, left, bottom or right)
39833 * and will simply describe the contents of your menu. To use this menu you will call various menu related functions on the {@link Ext.Viewport}
39834 * such as {@link Ext.Viewport#showMenu}, {@link Ext.Viewport#hideMenu}, {@link Ext.Viewport#toggleMenu}, {@link Ext.Viewport#hideOtherMenus},
39835 * or {@link Ext.Viewport#hideAllMenus}.
39836 *
39837 * @example preview
39838 * var menu = Ext.create('Ext.Menu', {
39839 * items: [
39840 * {
39841 * text: 'Settings',
39842 * iconCls: 'settings'
39843 * },
39844 * {
39845 * text: 'New Item',
39846 * iconCls: 'compose'
39847 * },
39848 * {
39849 * text: 'Star',
39850 * iconCls: 'star'
39851 * }
39852 * ]
39853 * });
39854 *
39855 * Ext.Viewport.setMenu(menu, {
39856 * side: 'left',
39857 * reveal: true
39858 * });
39859 *
39860 * Ext.Viewport.showMenu('left');
39861 *
39862 * The {@link #defaultType} of a Menu item is a {@link Ext.Button button}.
39863 */
39864 Ext.define('Ext.Menu', {
39865 extend: Ext.Sheet ,
39866 xtype: 'menu',
39867
39868
39869 config: {
39870 /**
39871 * @cfg
39872 * @inheritdoc
39873 */
39874 baseCls: Ext.baseCSSPrefix + 'menu',
39875
39876 /**
39877 * @cfg
39878 * @inheritdoc
39879 */
39880 left: 0,
39881
39882 /**
39883 * @cfg
39884 * @inheritdoc
39885 */
39886 right: 0,
39887
39888 /**
39889 * @cfg
39890 * @inheritdoc
39891 */
39892 bottom: 0,
39893
39894 /**
39895 * @cfg
39896 * @inheritdoc
39897 */
39898 height: 'auto',
39899
39900 /**
39901 * @cfg
39902 * @inheritdoc
39903 */
39904 width: 'auto',
39905
39906 /**
39907 * @cfg
39908 * @inheritdoc
39909 */
39910 defaultType: 'button',
39911
39912 /**
39913 * @hide
39914 */
39915 showAnimation: null,
39916
39917 /**
39918 * @hide
39919 */
39920 hideAnimation: null,
39921
39922 /**
39923 * @hide
39924 */
39925 centered: false,
39926
39927 /**
39928 * @hide
39929 */
39930 modal: true,
39931
39932 /**
39933 * @hide
39934 */
39935 hidden: true,
39936
39937 /**
39938 * @hide
39939 */
39940 hideOnMaskTap: true,
39941
39942 /**
39943 * @hide
39944 */
39945 translatable: {
39946 translationMethod: null
39947 }
39948 },
39949
39950 constructor: function() {
39951 this.config.translatable.translationMethod = Ext.browser.is.AndroidStock2 ? 'cssposition' : 'csstransform';
39952 this.callParent(arguments);
39953 },
39954
39955 platformConfig: [{
39956 theme: ['Windows']
39957 }, {
39958 theme: ['Blackberry', 'Blackberry103'],
39959 ui: 'context',
39960 layout: {
39961 pack: 'center'
39962 }
39963 }],
39964
39965 updateUi: function(newUi, oldUi) {
39966 this.callParent(arguments);
39967
39968 if (newUi != oldUi && (Ext.theme.is.Blackberry || Ext.theme.is.Blackberry103)) {
39969 if (newUi == 'context') {
39970 this.innerElement.swapCls('x-vertical', 'x-horizontal');
39971 }
39972 else if (newUi == 'application') {
39973 this.innerElement.swapCls('x-horizontal', 'x-vertical');
39974 }
39975 }
39976 },
39977
39978 updateHideOnMaskTap : function(hide) {
39979 var mask = this.getModal();
39980
39981 if (mask) {
39982 mask[hide ? 'on' : 'un'].call(mask, 'tap', function() {
39983 Ext.Viewport.hideMenu(this.$side);
39984 }, this);
39985 }
39986 },
39987
39988 /**
39989 * Only fire the hide event if it is initialized
39990 */
39991 doSetHidden: function() {
39992 if (this.initialized) {
39993 this.callParent(arguments);
39994 }
39995 }
39996 });
39997
39998 /**
39999 * {@link Ext.Title} is used for the {@link Ext.Toolbar#title} configuration in the {@link Ext.Toolbar} component.
40000 * @private
40001 */
40002 Ext.define('Ext.Title', {
40003 extend: Ext.Component ,
40004 xtype: 'title',
40005
40006 config: {
40007 /**
40008 * @cfg
40009 * @inheritdoc
40010 */
40011 baseCls: 'x-title',
40012
40013 /**
40014 * @cfg {String} title The title text
40015 */
40016 title: ''
40017 },
40018
40019 // @private
40020 updateTitle: function(newTitle) {
40021 this.setHtml(newTitle);
40022 }
40023 });
40024
40025 /**
40026 The {@link Ext.Spacer} component is generally used to put space between items in {@link Ext.Toolbar} components.
40027
40028 ## Examples
40029
40030 By default the {@link #flex} configuration is set to 1:
40031
40032 @example miniphone preview
40033 Ext.create('Ext.Container', {
40034 fullscreen: true,
40035 items: [
40036 {
40037 xtype : 'toolbar',
40038 docked: 'top',
40039 items: [
40040 {
40041 xtype: 'button',
40042 text : 'Button One'
40043 },
40044 {
40045 xtype: 'spacer'
40046 },
40047 {
40048 xtype: 'button',
40049 text : 'Button Two'
40050 }
40051 ]
40052 }
40053 ]
40054 });
40055
40056 Alternatively you can just set the {@link #width} configuration which will get the {@link Ext.Spacer} a fixed width:
40057
40058 @example preview
40059 Ext.create('Ext.Container', {
40060 fullscreen: true,
40061 layout: {
40062 type: 'vbox',
40063 pack: 'center',
40064 align: 'stretch'
40065 },
40066 items: [
40067 {
40068 xtype : 'toolbar',
40069 docked: 'top',
40070 items: [
40071 {
40072 xtype: 'button',
40073 text : 'Button One'
40074 },
40075 {
40076 xtype: 'spacer',
40077 width: 50
40078 },
40079 {
40080 xtype: 'button',
40081 text : 'Button Two'
40082 }
40083 ]
40084 },
40085 {
40086 xtype: 'container',
40087 items: [
40088 {
40089 xtype: 'button',
40090 text : 'Change Ext.Spacer width',
40091 handler: function() {
40092 //get the spacer using ComponentQuery
40093 var spacer = Ext.ComponentQuery.query('spacer')[0],
40094 from = 10,
40095 to = 250;
40096
40097 //set the width to a random number
40098 spacer.setWidth(Math.floor(Math.random() * (to - from + 1) + from));
40099 }
40100 }
40101 ]
40102 }
40103 ]
40104 });
40105
40106 You can also insert multiple {@link Ext.Spacer}'s:
40107
40108 @example preview
40109 Ext.create('Ext.Container', {
40110 fullscreen: true,
40111 items: [
40112 {
40113 xtype : 'toolbar',
40114 docked: 'top',
40115 items: [
40116 {
40117 xtype: 'button',
40118 text : 'Button One'
40119 },
40120 {
40121 xtype: 'spacer'
40122 },
40123 {
40124 xtype: 'button',
40125 text : 'Button Two'
40126 },
40127 {
40128 xtype: 'spacer',
40129 width: 20
40130 },
40131 {
40132 xtype: 'button',
40133 text : 'Button Three'
40134 }
40135 ]
40136 }
40137 ]
40138 });
40139 */
40140 Ext.define('Ext.Spacer', {
40141 extend: Ext.Component ,
40142 alias : 'widget.spacer',
40143
40144 config: {
40145 /**
40146 * @cfg {Number} flex
40147 * The flex value of this spacer. This defaults to 1, if no width has been set.
40148 * @accessor
40149 */
40150
40151 /**
40152 * @cfg {Number} width
40153 * The width of this spacer. If this is set, the value of {@link #flex} will be ignored.
40154 * @accessor
40155 */
40156 },
40157
40158 // @private
40159 constructor: function(config) {
40160 config = config || {};
40161
40162 if (!config.width) {
40163 config.flex = 1;
40164 }
40165
40166 this.callParent([config]);
40167 }
40168 });
40169
40170 /**
40171 * {@link Ext.Toolbar}s are most commonly used as docked items as within a {@link Ext.Container}. They can be docked either `top` or `bottom` using the {@link #docked} configuration.
40172 *
40173 * They allow you to insert items (normally {@link Ext.Button buttons}) and also add a {@link #title}.
40174 *
40175 * The {@link #defaultType} of {@link Ext.Toolbar} is {@link Ext.Button}.
40176 *
40177 * You can alternatively use {@link Ext.TitleBar} if you want the title to automatically adjust the size of its items.
40178 *
40179 * ## Examples
40180 *
40181 * @example miniphone preview
40182 * Ext.create('Ext.Container', {
40183 * fullscreen: true,
40184 * layout: {
40185 * type: 'vbox',
40186 * pack: 'center'
40187 * },
40188 * items: [
40189 * {
40190 * xtype : 'toolbar',
40191 * docked: 'top',
40192 * title: 'My Toolbar'
40193 * },
40194 * {
40195 * xtype: 'container',
40196 * defaults: {
40197 * xtype: 'button',
40198 * margin: '10 10 0 10'
40199 * },
40200 * items: [
40201 * {
40202 * text: 'Toggle docked',
40203 * handler: function() {
40204 * var toolbar = Ext.ComponentQuery.query('toolbar')[0],
40205 * newDocked = (toolbar.getDocked() === 'top') ? 'bottom' : 'top';
40206 *
40207 * toolbar.setDocked(newDocked);
40208 * }
40209 * },
40210 * {
40211 * text: 'Toggle UI',
40212 * handler: function() {
40213 * var toolbar = Ext.ComponentQuery.query('toolbar')[0],
40214 * newUi = (toolbar.getUi() === 'light') ? 'dark' : 'light';
40215 *
40216 * toolbar.setUi(newUi);
40217 * }
40218 * },
40219 * {
40220 * text: 'Change title',
40221 * handler: function() {
40222 * var toolbar = Ext.ComponentQuery.query('toolbar')[0],
40223 * titles = ['My Toolbar', 'Ext.Toolbar', 'Configurations are awesome!', 'Beautiful.'],
40224 //internally, the title configuration gets converted into a {@link Ext.Title} component,
40225 //so you must get the title configuration of that component
40226 * title = toolbar.getTitle().getTitle(),
40227 * newTitle = titles[titles.indexOf(title) + 1] || titles[0];
40228 *
40229 * toolbar.setTitle(newTitle);
40230 * }
40231 * }
40232 * ]
40233 * }
40234 * ]
40235 * });
40236 *
40237 * ## Notes
40238 *
40239 * You must use a HTML5 doctype for {@link #docked} `bottom` to work. To do this, simply add the following code to the HTML file:
40240 *
40241 * <!doctype html>
40242 *
40243 * So your index.html file should look a little like this:
40244 *
40245 * <!doctype html>
40246 * <html>
40247 * <head>
40248 * <title>MY application title</title>
40249 * ...
40250 *
40251 */
40252 Ext.define('Ext.Toolbar', {
40253 extend: Ext.Container ,
40254 xtype : 'toolbar',
40255
40256
40257
40258
40259
40260
40261
40262
40263 // @private
40264 isToolbar: true,
40265
40266 config: {
40267 /**
40268 * @cfg baseCls
40269 * @inheritdoc
40270 */
40271 baseCls: Ext.baseCSSPrefix + 'toolbar',
40272
40273 /**
40274 * @cfg {String} ui
40275 * The ui for this {@link Ext.Toolbar}. Either 'light' or 'dark'. You can create more UIs by using using the CSS Mixin {@link #sencha-toolbar-ui}
40276 * @accessor
40277 */
40278 ui: 'dark',
40279
40280 /**
40281 * @cfg {String/Ext.Title} title
40282 * The title of the toolbar.
40283 * @accessor
40284 */
40285 title: null,
40286
40287 /**
40288 * @cfg {String} defaultType
40289 * The default xtype to create.
40290 * @accessor
40291 */
40292 defaultType: 'button',
40293
40294 /**
40295 * @cfg {String} docked
40296 * The docked position for this {@link Ext.Toolbar}.
40297 * If you specify `left` or `right`, the {@link #layout} configuration will automatically change to a `vbox`. It's also
40298 * recommended to adjust the {@link #width} of the toolbar if you do this.
40299 * @accessor
40300 */
40301
40302 /**
40303 * @cfg {String} minHeight
40304 * The minimum height height of the Toolbar.
40305 * @accessor
40306 */
40307 minHeight: null,
40308
40309 /**
40310 * @cfg {Object/String} layout Configuration for this Container's layout. Example:
40311 *
40312 * Ext.create('Ext.Container', {
40313 * layout: {
40314 * type: 'hbox',
40315 * align: 'middle'
40316 * },
40317 * items: [
40318 * {
40319 * xtype: 'panel',
40320 * flex: 1,
40321 * style: 'background-color: red;'
40322 * },
40323 * {
40324 * xtype: 'panel',
40325 * flex: 2,
40326 * style: 'background-color: green'
40327 * }
40328 * ]
40329 * });
40330 *
40331 * See the [layouts guide](../../../core_concepts/layouts.html) for more information
40332 *
40333 * __Note:__ If you set the {@link #docked} configuration to `left` or `right`, the default layout will change from the
40334 * `hbox` to a `vbox`.
40335 *
40336 * @accessor
40337 */
40338 layout: {
40339 type: 'hbox',
40340 align: 'center'
40341 }
40342 },
40343
40344 hasCSSMinHeight: true,
40345
40346 constructor: function(config) {
40347 config = config || {};
40348
40349 if (config.docked == "left" || config.docked == "right") {
40350 config.layout = {
40351 type: 'vbox',
40352 align: 'stretch'
40353 };
40354 }
40355
40356 this.callParent([config]);
40357 },
40358
40359 // @private
40360 applyTitle: function(title) {
40361 if (typeof title == 'string') {
40362 title = {
40363 title: title,
40364 centered : Ext.theme.is.Tizen ? false : true
40365 };
40366 }
40367
40368 return Ext.factory(title, Ext.Title, this.getTitle());
40369 },
40370
40371 // @private
40372 updateTitle: function(newTitle, oldTitle) {
40373 if (newTitle) {
40374 this.add(newTitle);
40375 }
40376
40377 if (oldTitle) {
40378 oldTitle.destroy();
40379 }
40380 },
40381
40382 /**
40383 * Shows the title, if it exists.
40384 */
40385 showTitle: function() {
40386 var title = this.getTitle();
40387
40388 if (title) {
40389 title.show();
40390 }
40391 },
40392
40393 /**
40394 * Hides the title, if it exists.
40395 */
40396 hideTitle: function() {
40397 var title = this.getTitle();
40398
40399 if (title) {
40400 title.hide();
40401 }
40402 }
40403
40404 /**
40405 * Returns an {@link Ext.Title} component.
40406 * @member Ext.Toolbar
40407 * @method getTitle
40408 * @return {Ext.Title}
40409 */
40410
40411 /**
40412 * Use this to update the {@link #title} configuration.
40413 * @member Ext.Toolbar
40414 * @method setTitle
40415 * @param {String/Ext.Title} title You can either pass a String, or a config/instance of {@link Ext.Title}.
40416 */
40417
40418 }, function() {
40419 });
40420
40421
40422 /**
40423 * @private
40424 */
40425 Ext.define('Ext.field.Input', {
40426 extend: Ext.Component ,
40427 xtype : 'input',
40428
40429 /**
40430 * @event clearicontap
40431 * Fires whenever the clear icon is tapped.
40432 * @param {Ext.field.Input} this
40433 * @param {Ext.EventObject} e The event object
40434 */
40435
40436 /**
40437 * @event masktap
40438 * @preventable doMaskTap
40439 * Fires whenever a mask is tapped.
40440 * @param {Ext.field.Input} this
40441 * @param {Ext.EventObject} e The event object.
40442 */
40443
40444 /**
40445 * @event focus
40446 * @preventable doFocus
40447 * Fires whenever the input get focus.
40448 * @param {Ext.EventObject} e The event object.
40449 */
40450
40451 /**
40452 * @event blur
40453 * @preventable doBlur
40454 * Fires whenever the input loses focus.
40455 * @param {Ext.EventObject} e The event object.
40456 */
40457
40458 /**
40459 * @event click
40460 * Fires whenever the input is clicked.
40461 * @param {Ext.EventObject} e The event object.
40462 */
40463
40464 /**
40465 * @event keyup
40466 * Fires whenever keyup is detected.
40467 * @param {Ext.EventObject} e The event object.
40468 */
40469
40470 /**
40471 * @event paste
40472 * Fires whenever paste is detected.
40473 * @param {Ext.EventObject} e The event object.
40474 */
40475
40476 /**
40477 * @event mousedown
40478 * Fires whenever the input has a mousedown occur.
40479 * @param {Ext.EventObject} e The event object.
40480 */
40481
40482 /**
40483 * @property {String} tag The el tag.
40484 * @private
40485 */
40486 tag: 'input',
40487
40488 cachedConfig: {
40489 /**
40490 * @cfg {String} cls The `className` to be applied to this input.
40491 * @accessor
40492 */
40493 cls: Ext.baseCSSPrefix + 'form-field',
40494
40495 /**
40496 * @cfg {String} focusCls The CSS class to use when the field receives focus.
40497 * @accessor
40498 */
40499 focusCls: Ext.baseCSSPrefix + 'field-focus',
40500
40501 // @private
40502 maskCls: Ext.baseCSSPrefix + 'field-mask',
40503
40504 /**
40505 * @cfg {String/Boolean} useMask
40506 * `true` to use a mask on this field, or `auto` to automatically select when you should use it.
40507 * @private
40508 * @accessor
40509 */
40510 useMask: 'auto',
40511
40512 /**
40513 * @cfg {String} type The type attribute for input fields -- e.g. radio, text, password.
40514 *
40515 * If you want to use a `file` input, please use the {@link Ext.field.File} component instead.
40516 * @accessor
40517 */
40518 type: 'text',
40519
40520 /**
40521 * @cfg {Boolean} checked `true` if the checkbox should render initially checked.
40522 * @accessor
40523 */
40524 checked: false
40525 },
40526
40527 config: {
40528 /**
40529 * @cfg
40530 * @inheritdoc
40531 */
40532 baseCls: Ext.baseCSSPrefix + 'field-input',
40533
40534 /**
40535 * @cfg {String} name The field's HTML name attribute.
40536 * __Note:__ This property must be set if this field is to be automatically included with
40537 * {@link Ext.form.Panel#method-submit form submit()}.
40538 * @accessor
40539 */
40540 name: null,
40541
40542 /**
40543 * @cfg {Mixed} value A value to initialize this field with.
40544 * @accessor
40545 */
40546 value: null,
40547
40548 /**
40549 * @property {Boolean} `true` if the field currently has focus.
40550 * @accessor
40551 */
40552 isFocused: false,
40553
40554 /**
40555 * @cfg {Number} tabIndex The `tabIndex` for this field.
40556 *
40557 * __Note:__ This only applies to fields that are rendered, not those which are built via `applyTo`.
40558 * @accessor
40559 */
40560 tabIndex: null,
40561
40562 /**
40563 * @cfg {String} placeHolder A string value displayed in the input (if supported) when the control is empty.
40564 * @accessor
40565 */
40566 placeHolder: null,
40567
40568 /**
40569 * @cfg {Number} [minValue=undefined] The minimum value that this Number field can accept (defaults to `undefined`, e.g. no minimum).
40570 * @accessor
40571 */
40572 minValue: null,
40573
40574 /**
40575 * @cfg {Number} [maxValue=undefined] The maximum value that this Number field can accept (defaults to `undefined`, e.g. no maximum).
40576 * @accessor
40577 */
40578 maxValue: null,
40579
40580 /**
40581 * @cfg {Number} [stepValue=undefined] The amount by which the field is incremented or decremented each time the spinner is tapped.
40582 * Defaults to `undefined`, which means that the field goes up or down by 1 each time the spinner is tapped.
40583 * @accessor
40584 */
40585 stepValue: null,
40586
40587 /**
40588 * @cfg {Number} [maxLength=0] The maximum number of permitted input characters.
40589 * @accessor
40590 */
40591 maxLength: null,
40592
40593 /**
40594 * @cfg {Boolean} [autoComplete=undefined]
40595 * `true` to set the field's DOM element `autocomplete` attribute to `"on"`, `false` to set to `"off"`. Defaults to `undefined`, leaving the attribute unset.
40596 * @accessor
40597 */
40598 autoComplete: null,
40599
40600 /**
40601 * @cfg {Boolean} [autoCapitalize=undefined]
40602 * `true` to set the field's DOM element `autocapitalize` attribute to `"on"`, `false` to set to `"off"`. Defaults to `undefined`, leaving the attribute unset
40603 * @accessor
40604 */
40605 autoCapitalize: null,
40606
40607 /**
40608 * `true` to set the field DOM element `autocorrect` attribute to `"on"`, `false` to set to `"off"`. Defaults to `undefined`, leaving the attribute unset.
40609 * @cfg {Boolean} autoCorrect
40610 * @accessor
40611 */
40612 autoCorrect: null,
40613
40614 /**
40615 * @cfg {Boolean} [readOnly=undefined]
40616 * `true` to set the field DOM element `readonly` attribute to `"true"`. Defaults to `undefined`, leaving the attribute unset.
40617 * @accessor
40618 */
40619 readOnly: null,
40620
40621 /**
40622 * @cfg {Number} [maxRows=undefined]
40623 * Sets the field DOM element `maxRows` attribute. Defaults to `undefined`, leaving the attribute unset.
40624 * @accessor
40625 */
40626 maxRows: null,
40627
40628 /**
40629 * @cfg {String} pattern The value for the HTML5 `pattern` attribute.
40630 * You can use this to change which keyboard layout will be used.
40631 *
40632 * Ext.define('Ux.field.Pattern', {
40633 * extend : 'Ext.field.Text',
40634 * xtype : 'patternfield',
40635 *
40636 * config : {
40637 * component : {
40638 * pattern : '[0-9]*'
40639 * }
40640 * }
40641 * });
40642 *
40643 * Even though it extends {@link Ext.field.Text}, it will display the number keyboard.
40644 *
40645 * @accessor
40646 */
40647 pattern: null,
40648
40649 /**
40650 * @cfg {Boolean} [disabled=false] `true` to disable the field.
40651 *
40652 * Be aware that conformant with the [HTML specification](http://www.w3.org/TR/html401/interact/forms.html),
40653 * disabled Fields will not be {@link Ext.form.Panel#method-submit submitted}.
40654 * @accessor
40655 */
40656
40657 /**
40658 * @cfg {Mixed} startValue
40659 * The value that the Field had at the time it was last focused. This is the value that is passed
40660 * to the {@link Ext.field.Text#change} event which is fired if the value has been changed when the Field is blurred.
40661 *
40662 * __This will be `undefined` until the Field has been visited.__ Compare {@link #originalValue}.
40663 * @accessor
40664 */
40665 startValue: false,
40666
40667 /**
40668 * @cfg {Boolean} fastFocus
40669 *
40670 * Enable Fast Input Focusing on iOS, using this workaround will stop some touchstart events in order to prevent
40671 * delayed focus issues.
40672 *
40673 * @accessor
40674 */
40675 fastFocus: true
40676 },
40677
40678 /**
40679 * @cfg {String/Number} originalValue The original value when the input is rendered.
40680 * @private
40681 */
40682
40683 // @private
40684 getTemplate: function() {
40685 var items = [
40686 {
40687 reference: 'input',
40688 tag: this.tag
40689 },
40690 {
40691 reference: 'mask',
40692 classList: [this.config.maskCls]
40693 },
40694 {
40695 reference: 'clearIcon',
40696 cls: 'x-clear-icon'
40697 }
40698 ];
40699
40700 return items;
40701 },
40702
40703 initElement: function() {
40704 var me = this;
40705
40706 me.callParent();
40707
40708 me.input.on({
40709 scope: me,
40710
40711 keyup: 'onKeyUp',
40712 keydown: 'onKeyDown',
40713 focus: 'onFocus',
40714 blur: 'onBlur',
40715 input: 'onInput',
40716 paste: 'onPaste',
40717 tap: 'onInputTap'
40718 });
40719
40720
40721 // Stock android has a delayed mousedown event that is dispatched
40722 // this prevents the mousedown from focus's an input when not intended (click a message box button or picker button that lays over an input)
40723 // we then force focus on touchend.
40724 if(Ext.browser.is.AndroidStock) {
40725 me.input.dom.addEventListener("mousedown", function(e) {
40726 if(document.activeElement != e.target) {
40727 e.preventDefault();
40728 }
40729 } );
40730 me.input.dom.addEventListener("touchend", function() { me.focus(); });
40731 }
40732
40733 me.mask.on({
40734 scope: me,
40735 tap: 'onMaskTap'
40736 });
40737
40738 if (me.clearIcon) {
40739 me.clearIcon.on({
40740 tap: 'onClearIconTap',
40741 touchstart: 'onClearIconPress',
40742 touchend: 'onClearIconRelease',
40743 scope: me
40744 });
40745 }
40746
40747 // Hack for IE10. Seems like keyup event is not fired for 'enter' keyboard button, so we use keypress event instead to handle enter.
40748 if(Ext.browser.is.ie && Ext.browser.version.major >=10){
40749 me.input.on({
40750 scope: me,
40751 keypress: 'onKeyPress'
40752 });
40753 }
40754 },
40755
40756 updateFastFocus: function(newValue) {
40757 // This is used to prevent 300ms delayed focus bug on iOS
40758 if (newValue) {
40759 if (this.getFastFocus() && Ext.os.is.iOS) {
40760 this.input.on({
40761 scope: this,
40762 touchstart: "onTouchStart"
40763 });
40764 }
40765 } else {
40766 this.input.un({
40767 scope: this,
40768 touchstart: "onTouchStart"
40769 });
40770 }
40771 },
40772
40773 /**
40774 * Manual Max Length processing is required for the stock "Browser" on Android
40775 * @private
40776 * @return {Boolean} 'true' if non-chrome browser is detected on Android
40777 */
40778 useManualMaxLength: function() {
40779 return Boolean((Ext.os.is.Android && !Ext.browser.is.Chrome));
40780 },
40781
40782 applyUseMask: function(useMask) {
40783 if (useMask === 'auto') {
40784 useMask = Ext.os.is.iOS && Ext.os.version.lt('5');
40785 }
40786
40787 return Boolean(useMask);
40788 },
40789
40790 /**
40791 * Updates the useMask configuration
40792 */
40793 updateUseMask: function(newUseMask) {
40794 this.mask[newUseMask ? 'show' : 'hide']();
40795 },
40796
40797 updatePattern : function (pattern) {
40798 this.updateFieldAttribute('pattern', pattern);
40799 },
40800
40801 /**
40802 * Helper method to update a specified attribute on the `fieldEl`, or remove the attribute all together.
40803 * @private
40804 */
40805 updateFieldAttribute: function(attribute, newValue) {
40806 var input = this.input;
40807
40808 if (!Ext.isEmpty(newValue, true)) {
40809 input.dom.setAttribute(attribute, newValue);
40810 } else {
40811 input.dom.removeAttribute(attribute);
40812 }
40813 },
40814
40815 /**
40816 * Updates the {@link #cls} configuration.
40817 */
40818 updateCls: function(newCls, oldCls) {
40819 this.input.addCls(Ext.baseCSSPrefix + 'input-el');
40820 this.input.replaceCls(oldCls, newCls);
40821 },
40822
40823 /**
40824 * Updates the type attribute with the {@link #type} configuration.
40825 * @private
40826 */
40827 updateType: function(newType, oldType) {
40828 var prefix = Ext.baseCSSPrefix + 'input-';
40829
40830 this.input.replaceCls(prefix + oldType, prefix + newType);
40831 this.updateFieldAttribute('type', newType);
40832 },
40833
40834 /**
40835 * Updates the name attribute with the {@link #name} configuration.
40836 * @private
40837 */
40838 updateName: function(newName) {
40839 this.updateFieldAttribute('name', newName);
40840 },
40841
40842 /**
40843 * Returns the field data value.
40844 * @return {Mixed} value The field value.
40845 */
40846 getValue: function() {
40847 var input = this.input;
40848
40849 if (input) {
40850 this._value = input.dom.value;
40851 }
40852
40853 return this._value;
40854 },
40855
40856 // @private
40857 applyValue: function(value) {
40858 return (Ext.isEmpty(value)) ? '' : value;
40859 },
40860
40861 /**
40862 * Updates the {@link #value} configuration.
40863 * @private
40864 */
40865 updateValue: function(newValue) {
40866 var input = this.input;
40867
40868 if (input) {
40869 input.dom.value = newValue;
40870 }
40871 },
40872
40873 setValue: function(newValue) {
40874 var oldValue = this._value;
40875
40876 this.updateValue(this.applyValue(newValue));
40877
40878 newValue = this.getValue();
40879
40880 if (String(newValue) != String(oldValue) && this.initialized) {
40881 this.onChange(this, newValue, oldValue);
40882 }
40883
40884 return this;
40885 },
40886
40887 //<debug>
40888 // @private
40889 applyTabIndex: function(tabIndex) {
40890 if (tabIndex !== null && typeof tabIndex != 'number') {
40891 throw new Error("Ext.field.Field: [applyTabIndex] trying to pass a value which is not a number");
40892 }
40893 return tabIndex;
40894 },
40895 //</debug>
40896
40897 /**
40898 * Updates the tabIndex attribute with the {@link #tabIndex} configuration
40899 * @private
40900 */
40901 updateTabIndex: function(newTabIndex) {
40902 this.updateFieldAttribute('tabIndex', newTabIndex);
40903 },
40904
40905 // @private
40906 testAutoFn: function(value) {
40907 return [true, 'on'].indexOf(value) !== -1;
40908 },
40909
40910 //<debug>
40911 applyMaxLength: function(maxLength) {
40912 if (maxLength !== null && typeof maxLength != 'number') {
40913 throw new Error("Ext.field.Text: [applyMaxLength] trying to pass a value which is not a number");
40914 }
40915 return maxLength;
40916 },
40917 //</debug>
40918
40919 /**
40920 * Updates the `maxlength` attribute with the {@link #maxLength} configuration.
40921 * @private
40922 */
40923 updateMaxLength: function(newMaxLength) {
40924 if (!this.useManualMaxLength()) {
40925 this.updateFieldAttribute('maxlength', newMaxLength);
40926 }
40927 },
40928
40929 /**
40930 * Updates the `placeholder` attribute with the {@link #placeHolder} configuration.
40931 * @private
40932 */
40933 updatePlaceHolder: function(newPlaceHolder) {
40934 this.updateFieldAttribute('placeholder', newPlaceHolder);
40935 },
40936
40937 // @private
40938 applyAutoComplete: function(autoComplete) {
40939 return this.testAutoFn(autoComplete);
40940 },
40941
40942 /**
40943 * Updates the `autocomplete` attribute with the {@link #autoComplete} configuration.
40944 * @private
40945 */
40946 updateAutoComplete: function(newAutoComplete) {
40947 var value = newAutoComplete ? 'on' : 'off';
40948 this.updateFieldAttribute('autocomplete', value);
40949 },
40950
40951 // @private
40952 applyAutoCapitalize: function(autoCapitalize) {
40953 return this.testAutoFn(autoCapitalize);
40954 },
40955
40956 /**
40957 * Updates the `autocapitalize` attribute with the {@link #autoCapitalize} configuration.
40958 * @private
40959 */
40960 updateAutoCapitalize: function(newAutoCapitalize) {
40961 var value = newAutoCapitalize ? 'on' : 'off';
40962 this.updateFieldAttribute('autocapitalize', value);
40963 },
40964
40965 // @private
40966 applyAutoCorrect: function(autoCorrect) {
40967 return this.testAutoFn(autoCorrect);
40968 },
40969
40970 /**
40971 * Updates the `autocorrect` attribute with the {@link #autoCorrect} configuration.
40972 * @private
40973 */
40974 updateAutoCorrect: function(newAutoCorrect) {
40975 var value = newAutoCorrect ? 'on' : 'off';
40976 this.updateFieldAttribute('autocorrect', value);
40977 },
40978
40979 /**
40980 * Updates the `min` attribute with the {@link #minValue} configuration.
40981 * @private
40982 */
40983 updateMinValue: function(newMinValue) {
40984 this.updateFieldAttribute('min', newMinValue);
40985 },
40986
40987 /**
40988 * Updates the `max` attribute with the {@link #maxValue} configuration.
40989 * @private
40990 */
40991 updateMaxValue: function(newMaxValue) {
40992 this.updateFieldAttribute('max', newMaxValue);
40993 },
40994
40995 /**
40996 * Updates the `step` attribute with the {@link #stepValue} configuration
40997 * @private
40998 */
40999 updateStepValue: function(newStepValue) {
41000 this.updateFieldAttribute('step', newStepValue);
41001 },
41002
41003 // @private
41004 checkedRe: /^(true|1|on)/i,
41005
41006 /**
41007 * Returns the `checked` value of this field
41008 * @return {Mixed} value The field value
41009 */
41010 getChecked: function() {
41011 var el = this.input,
41012 checked;
41013
41014 if (el) {
41015 checked = el.dom.checked;
41016 this._checked = checked;
41017 }
41018
41019 return checked;
41020 },
41021
41022 // @private
41023 applyChecked: function(checked) {
41024 return !!this.checkedRe.test(String(checked));
41025 },
41026
41027 setChecked: function(newChecked) {
41028 this.updateChecked(this.applyChecked(newChecked));
41029 this._checked = newChecked;
41030 },
41031
41032 /**
41033 * Updates the `autocorrect` attribute with the {@link #autoCorrect} configuration
41034 * @private
41035 */
41036 updateChecked: function(newChecked) {
41037 this.input.dom.checked = newChecked;
41038 },
41039
41040 /**
41041 * Updates the `readonly` attribute with the {@link #readOnly} configuration
41042 * @private
41043 */
41044 updateReadOnly: function(readOnly) {
41045 this.updateFieldAttribute('readonly', readOnly ? true : null);
41046 },
41047
41048 //<debug>
41049 // @private
41050 applyMaxRows: function(maxRows) {
41051 if (maxRows !== null && typeof maxRows !== 'number') {
41052 throw new Error("Ext.field.Input: [applyMaxRows] trying to pass a value which is not a number");
41053 }
41054
41055 return maxRows;
41056 },
41057 //</debug>
41058
41059 updateMaxRows: function(newRows) {
41060 this.updateFieldAttribute('rows', newRows);
41061 },
41062
41063 doSetDisabled: function(disabled) {
41064 this.callParent(arguments);
41065
41066 if (Ext.browser.is.Safari && !Ext.os.is.BlackBerry) {
41067 this.input.dom.tabIndex = (disabled) ? -1 : 0;
41068 }
41069
41070 this.input.dom.disabled = (Ext.browser.is.Safari && !Ext.os.is.BlackBerry) ? false : disabled;
41071
41072 if (!disabled) {
41073 this.blur();
41074 }
41075 },
41076
41077 /**
41078 * Returns `true` if the value of this Field has been changed from its original value.
41079 * Will return `false` if the field is disabled or has not been rendered yet.
41080 * @return {Boolean}
41081 */
41082 isDirty: function() {
41083 if (this.getDisabled()) {
41084 return false;
41085 }
41086
41087 return String(this.getValue()) !== String(this.originalValue);
41088 },
41089
41090 /**
41091 * Resets the current field value to the original value.
41092 */
41093 reset: function() {
41094 this.setValue(this.originalValue);
41095 },
41096
41097 // @private
41098 onInputTap: function(e) {
41099 this.fireAction('inputtap', [this, e], 'doInputTap');
41100 },
41101
41102 // @private
41103 doInputTap: function(me, e) {
41104 if (me.getDisabled()) {
41105 return false;
41106 }
41107
41108 // Fast focus switching
41109 if (this.getFastFocus() && Ext.os.is.iOS) {
41110 me.focus();
41111 }
41112 },
41113
41114 // @private
41115 onMaskTap: function(e) {
41116 this.fireAction('masktap', [this, e], 'doMaskTap');
41117 },
41118
41119 // @private
41120 doMaskTap: function(me, e) {
41121 if (me.getDisabled()) {
41122 return false;
41123 }
41124
41125 me.focus();
41126 },
41127
41128 // @private
41129 showMask: function() {
41130 if (this.getUseMask()) {
41131 this.mask.setStyle('display', 'block');
41132 }
41133 },
41134
41135 // @private
41136 hideMask: function() {
41137 if (this.getUseMask()) {
41138 this.mask.setStyle('display', 'none');
41139 }
41140 },
41141
41142 /**
41143 * Attempts to set the field as the active input focus.
41144 * @return {Ext.field.Input} this
41145 */
41146 focus: function() {
41147 var me = this,
41148 el = me.input;
41149
41150 if (el && el.dom.focus) {
41151 el.dom.focus();
41152 }
41153 return me;
41154 },
41155
41156 /**
41157 * Attempts to forcefully blur input focus for the field.
41158 * @return {Ext.field.Input} this
41159 * @chainable
41160 */
41161 blur: function() {
41162 var me = this,
41163 el = this.input;
41164
41165 if (el && el.dom.blur) {
41166 el.dom.blur();
41167 }
41168 return me;
41169 },
41170
41171 /**
41172 * Attempts to forcefully select all the contents of the input field.
41173 * @return {Ext.field.Input} this
41174 * @chainable
41175 */
41176 select: function() {
41177 var me = this,
41178 el = me.input;
41179
41180 if (el && el.dom.setSelectionRange) {
41181 el.dom.setSelectionRange(0, 9999);
41182 }
41183 return me;
41184 },
41185
41186 onFocus: function(e) {
41187 this.fireAction('focus', [e], 'doFocus');
41188 },
41189
41190 // @private
41191 doFocus: function(e) {
41192 var me = this;
41193
41194 me.hideMask();
41195
41196 if (!me.getIsFocused()) {
41197 me.setStartValue(me.getValue());
41198 }
41199 me.setIsFocused(true);
41200 },
41201
41202 onTouchStart: function(e) {
41203 // This will prevent 300ms delayed focus from occurring on iOS
41204 if(document.activeElement != e.target) {
41205 e.preventDefault();
41206 }
41207 },
41208
41209 onBlur: function(e) {
41210 this.fireAction('blur', [e], 'doBlur');
41211 },
41212
41213 // @private
41214 doBlur: function(e) {
41215 var me = this,
41216 value = me.getValue(),
41217 startValue = me.getStartValue();
41218
41219 me.showMask();
41220
41221 me.setIsFocused(false);
41222
41223 if (String(value) != String(startValue)) {
41224 me.onChange(me, value, startValue);
41225 }
41226
41227 },
41228
41229 // @private
41230 onClearIconTap: function(e) {
41231 this.fireEvent('clearicontap', this, e);
41232
41233 //focus the field after cleartap happens, but only on android.
41234 //this is to stop the keyboard from hiding. TOUCH-2064
41235 if (Ext.os.is.Android) {
41236 this.focus();
41237 }
41238 },
41239
41240 onClearIconPress: function() {
41241 this.clearIcon.addCls(Ext.baseCSSPrefix + 'pressing');
41242 },
41243
41244 onClearIconRelease: function() {
41245 this.clearIcon.removeCls(Ext.baseCSSPrefix + 'pressing');
41246 },
41247
41248 onClick: function(e) {
41249 this.fireEvent('click', e);
41250 },
41251
41252 onChange: function(me, value, startValue) {
41253 if (this.useManualMaxLength()) {
41254 this.trimValueToMaxLength();
41255 }
41256 this.fireEvent('change', me, value, startValue);
41257 },
41258
41259 onPaste: function(e) {
41260 if (this.useManualMaxLength()) {
41261 this.trimValueToMaxLength();
41262 }
41263 this.fireEvent('paste', e);
41264 },
41265
41266 onKeyUp: function(e) {
41267 if (this.useManualMaxLength()) {
41268 this.trimValueToMaxLength();
41269 }
41270 this.fireEvent('keyup', e);
41271 },
41272
41273 onKeyDown: function() {
41274 // tell the class to ignore the input event. this happens when we want to listen to the field change
41275 // when the input autocompletes
41276 this.ignoreInput = true;
41277 },
41278
41279 onInput: function(e) {
41280 var me = this;
41281
41282 // if we should ignore input, stop now.
41283 if (me.ignoreInput) {
41284 me.ignoreInput = false;
41285 return;
41286 }
41287
41288 // set a timeout for 10ms to check if we want to stop the input event.
41289 // if not, then continue with the event (keyup)
41290 setTimeout(function() {
41291 if (!me.ignoreInput) {
41292 me.fireEvent('keyup', e);
41293 me.ignoreInput = false;
41294 }
41295 }, 10);
41296 },
41297
41298 // Hack for IE10 mobile. Handle pressing 'enter' button and fire keyup event in this case.
41299 onKeyPress: function(e) {
41300 if(e.browserEvent.keyCode == 13){
41301 this.fireEvent('keyup', e);
41302 }
41303 },
41304
41305 onMouseDown: function(e) {
41306 this.fireEvent('mousedown', e);
41307 },
41308
41309 trimValueToMaxLength: function() {
41310 var maxLength = this.getMaxLength();
41311 if (maxLength) {
41312 var value = this.getValue();
41313 if (value.length > this.getMaxLength()) {
41314 this.setValue(value.slice(0, maxLength));
41315 }
41316 }
41317 }
41318 });
41319
41320 /**
41321 * Field is the base class for all form fields used in Sencha Touch. It provides a lot of shared functionality to all
41322 * field subclasses (for example labels, simple validation, {@link #clearIcon clearing} and tab index management), but
41323 * is rarely used directly. Instead, it is much more common to use one of the field subclasses:
41324 *
41325 * xtype Class
41326 * ---------------------------------------
41327 * textfield {@link Ext.field.Text}
41328 * numberfield {@link Ext.field.Number}
41329 * textareafield {@link Ext.field.TextArea}
41330 * hiddenfield {@link Ext.field.Hidden}
41331 * radiofield {@link Ext.field.Radio}
41332 * filefield {@link Ext.field.File}
41333 * checkboxfield {@link Ext.field.Checkbox}
41334 * selectfield {@link Ext.field.Select}
41335 * togglefield {@link Ext.field.Toggle}
41336 * fieldset {@link Ext.form.FieldSet}
41337 *
41338 * Fields are normally used within the context of a form and/or fieldset. See the {@link Ext.form.Panel FormPanel}
41339 * and {@link Ext.form.FieldSet FieldSet} docs for examples on how to put those together, or the list of links above
41340 * for usage of individual field types. If you wish to create your own Field subclasses you can extend this class,
41341 * though it is sometimes more useful to extend {@link Ext.field.Text} as this provides additional text entry
41342 * functionality.
41343 *
41344 * For more information regarding forms and fields, please review [Using Forms in Sencha Touch Guide](../../../components/forms.html)
41345 */
41346 Ext.define('Ext.field.Field', {
41347 extend: Ext.Decorator ,
41348 alternateClassName: 'Ext.form.Field',
41349 xtype: 'field',
41350
41351
41352
41353
41354 /**
41355 * Set to `true` on all Ext.field.Field subclasses. This is used by {@link Ext.form.Panel#getValues} to determine which
41356 * components inside a form are fields.
41357 * @property isField
41358 * @type Boolean
41359 */
41360 isField: true,
41361
41362 // @private
41363 isFormField: true,
41364
41365 config: {
41366 /**
41367 * @cfg
41368 * @inheritdoc
41369 */
41370 baseCls: Ext.baseCSSPrefix + 'field',
41371
41372 /**
41373 * The label of this field
41374 * @cfg {String} label
41375 * @accessor
41376 */
41377 label: null,
41378
41379 /**
41380 * @cfg {String} labelAlign The position to render the label relative to the field input.
41381 * Available options are: 'top', 'left', 'bottom' and 'right'
41382 * @accessor
41383 */
41384 labelAlign: 'left',
41385
41386 /**
41387 * @cfg {Number/String} labelWidth The width to make this field's label.
41388 * @accessor
41389 */
41390 labelWidth: '30%',
41391
41392 /**
41393 * @cfg {Boolean} labelWrap `true` to allow the label to wrap. If set to `false`, the label will be truncated with
41394 * an ellipsis.
41395 * @accessor
41396 */
41397 labelWrap: false,
41398
41399 /**
41400 * @cfg {Boolean} clearIcon `true` to use a clear icon in this field.
41401 * @accessor
41402 */
41403 clearIcon: null,
41404
41405 /**
41406 * @cfg {Boolean} required `true` to make this field required.
41407 *
41408 * __Note:__ this only causes a visual indication.
41409 *
41410 * Doesn't prevent user from submitting the form.
41411 * @accessor
41412 */
41413 required: false,
41414
41415 /**
41416 * The label Element associated with this Field.
41417 *
41418 * __Note:__ Only available if a {@link #label} is specified.
41419 * @type Ext.Element
41420 * @property labelEl
41421 * @deprecated 2.0
41422 */
41423
41424 /**
41425 * @cfg {String} [inputType='text'] The type attribute for input fields -- e.g. radio, text, password, file.
41426 * The types 'file' and 'password' must be used to render those field types currently -- there are
41427 * no separate Ext components for those.
41428 * @deprecated 2.0 Please use `input.type` instead.
41429 * @accessor
41430 */
41431 inputType: null,
41432
41433 /**
41434 * @cfg {String} name The field's HTML name attribute.
41435 *
41436 * __Note:__ this property must be set if this field is to be automatically included with.
41437 * {@link Ext.form.Panel#method-submit form submit()}.
41438 * @accessor
41439 */
41440 name: null,
41441
41442 /**
41443 * @cfg {Mixed} value A value to initialize this field with.
41444 * @accessor
41445 */
41446 value: null,
41447
41448 /**
41449 * @cfg {Number} tabIndex The `tabIndex` for this field. Note this only applies to fields that are rendered,
41450 * not those which are built via `applyTo`.
41451 * @accessor
41452 */
41453 tabIndex: null
41454
41455 /**
41456 * @cfg {Object} component The inner component for this field.
41457 */
41458
41459 /**
41460 * @cfg {Boolean} fullscreen
41461 * @hide
41462 */
41463 },
41464
41465 platformConfig: [{
41466 theme: ['Windows', 'MountainView', 'Blackberry', 'Blackberry103', 'Tizen'],
41467 labelAlign: 'top'
41468 }],
41469
41470 cachedConfig: {
41471 /**
41472 * @cfg {String} labelCls Optional CSS class to add to the Label element.
41473 * @accessor
41474 */
41475 labelCls: null,
41476
41477 /**
41478 * @cfg {String} requiredCls The `className` to be applied to this Field when the {@link #required} configuration is set to `true`.
41479 * @accessor
41480 */
41481 requiredCls: Ext.baseCSSPrefix + 'field-required',
41482
41483 /**
41484 * @cfg {String} inputCls CSS class to add to the input element of this fields {@link #component}
41485 */
41486 inputCls: null
41487 },
41488
41489 /**
41490 * @cfg {Boolean} isFocused
41491 * `true` if this field is currently focused.
41492 * @private
41493 */
41494
41495 getElementConfig: function() {
41496 var prefix = Ext.baseCSSPrefix;
41497
41498 return {
41499 reference: 'element',
41500 className: 'x-container',
41501 children: [
41502 {
41503 reference: 'label',
41504 cls: prefix + 'form-label',
41505 children: [{
41506 reference: 'labelspan',
41507 tag: 'span'
41508 }]
41509 },
41510 {
41511 reference: 'innerElement',
41512 cls: prefix + 'component-outer'
41513 }
41514 ]
41515 };
41516 },
41517
41518 // @private
41519 updateLabel: function(newLabel, oldLabel) {
41520 var renderElement = this.renderElement,
41521 prefix = Ext.baseCSSPrefix;
41522
41523 if (newLabel) {
41524 this.labelspan.setHtml(newLabel);
41525 renderElement.addCls(prefix + 'field-labeled');
41526 } else {
41527 renderElement.removeCls(prefix + 'field-labeled');
41528 }
41529 },
41530
41531 // @private
41532 updateLabelAlign: function(newLabelAlign, oldLabelAlign) {
41533 var renderElement = this.renderElement,
41534 prefix = Ext.baseCSSPrefix;
41535
41536 if (newLabelAlign) {
41537 renderElement.addCls(prefix + 'label-align-' + newLabelAlign);
41538
41539 if (newLabelAlign == "top" || newLabelAlign == "bottom") {
41540 this.label.setWidth('100%');
41541 } else {
41542 this.updateLabelWidth(this.getLabelWidth());
41543 }
41544 }
41545
41546 if (oldLabelAlign) {
41547 renderElement.removeCls(prefix + 'label-align-' + oldLabelAlign);
41548 }
41549 },
41550
41551 // @private
41552 updateLabelCls: function(newLabelCls, oldLabelCls) {
41553 if (newLabelCls) {
41554 this.label.addCls(newLabelCls);
41555 }
41556
41557 if (oldLabelCls) {
41558 this.label.removeCls(oldLabelCls);
41559 }
41560 },
41561
41562 // @private
41563 updateLabelWidth: function(newLabelWidth) {
41564 var labelAlign = this.getLabelAlign();
41565
41566 if (newLabelWidth) {
41567 if (labelAlign == "top" || labelAlign == "bottom") {
41568 this.label.setWidth('100%');
41569 } else {
41570 this.label.setWidth(newLabelWidth);
41571 }
41572 }
41573 },
41574
41575 // @private
41576 updateLabelWrap: function(newLabelWrap, oldLabelWrap) {
41577 var cls = Ext.baseCSSPrefix + 'form-label-nowrap';
41578
41579 if (!newLabelWrap) {
41580 this.addCls(cls);
41581 } else {
41582 this.removeCls(cls);
41583 }
41584 },
41585
41586 /**
41587 * Updates the {@link #required} configuration.
41588 * @private
41589 */
41590 updateRequired: function(newRequired) {
41591 this.renderElement[newRequired ? 'addCls' : 'removeCls'](this.getRequiredCls());
41592 },
41593
41594 /**
41595 * Updates the {@link #required} configuration
41596 * @private
41597 */
41598 updateRequiredCls: function(newRequiredCls, oldRequiredCls) {
41599 if (this.getRequired()) {
41600 this.renderElement.replaceCls(oldRequiredCls, newRequiredCls);
41601 }
41602 },
41603
41604 // @private
41605 initialize: function() {
41606 var me = this;
41607 me.callParent();
41608
41609 me.doInitValue();
41610 },
41611
41612 /**
41613 * @private
41614 */
41615 doInitValue: function() {
41616 /**
41617 * @property {Mixed} originalValue
41618 * The original value of the field as configured in the {@link #value} configuration.
41619 * setting is `true`.
41620 */
41621 this.originalValue = this.getInitialConfig().value;
41622 },
41623
41624 /**
41625 * Resets the current field value back to the original value on this field when it was created.
41626 *
41627 * // This will create a field with an original value
41628 * var field = Ext.Viewport.add({
41629 * xtype: 'textfield',
41630 * value: 'first value'
41631 * });
41632 *
41633 * // Update the value
41634 * field.setValue('new value');
41635 *
41636 * // Now you can reset it back to the `first value`
41637 * field.reset();
41638 *
41639 * @return {Ext.field.Field} this
41640 */
41641 reset: function() {
41642 this.setValue(this.originalValue);
41643
41644 return this;
41645 },
41646
41647 /**
41648 * Resets the field's {@link #originalValue} property so it matches the current {@link #getValue value}. This is
41649 * called by {@link Ext.form.Panel}.{@link Ext.form.Panel#setValues setValues} if the form's
41650 * {@link Ext.form.Panel#trackResetOnLoad trackResetOnLoad} property is set to true.
41651 */
41652 resetOriginalValue: function() {
41653 this.originalValue = this.getValue();
41654 },
41655
41656 /**
41657 * Returns `true` if the value of this Field has been changed from its {@link #originalValue}.
41658 * Will return `false` if the field is disabled or has not been rendered yet.
41659 *
41660 * @return {Boolean} `true` if this field has been changed from its original value (and
41661 * is not disabled), `false` otherwise.
41662 */
41663 isDirty: function() {
41664 return false;
41665 }
41666 }, function() {
41667 });
41668
41669 /**
41670 * The text field is the basis for most of the input fields in Sencha Touch. It provides a baseline of shared
41671 * functionality such as input validation, standard events, state management and look and feel. Typically we create
41672 * text fields inside a form, like this:
41673 *
41674 * @example
41675 * Ext.create('Ext.form.Panel', {
41676 * fullscreen: true,
41677 * items: [
41678 * {
41679 * xtype: 'fieldset',
41680 * title: 'Enter your name',
41681 * items: [
41682 * {
41683 * xtype: 'textfield',
41684 * label: 'First Name',
41685 * name: 'firstName'
41686 * },
41687 * {
41688 * xtype: 'textfield',
41689 * label: 'Last Name',
41690 * name: 'lastName'
41691 * }
41692 * ]
41693 * }
41694 * ]
41695 * });
41696 *
41697 * This creates two text fields inside a form. Text Fields can also be created outside of a Form, like this:
41698 *
41699 * Ext.create('Ext.field.Text', {
41700 * label: 'Your Name',
41701 * value: 'Ed Spencer'
41702 * });
41703 *
41704 * ## Configuring
41705 *
41706 * Text field offers several configuration options, including {@link #placeHolder}, {@link #maxLength},
41707 * {@link #autoComplete}, {@link #autoCapitalize} and {@link #autoCorrect}. For example, here is how we would configure
41708 * a text field to have a maximum length of 10 characters, with placeholder text that disappears when the field is
41709 * focused:
41710 *
41711 * Ext.create('Ext.field.Text', {
41712 * label: 'Username',
41713 * maxLength: 10,
41714 * placeHolder: 'Enter your username'
41715 * });
41716 *
41717 * The autoComplete, autoCapitalize and autoCorrect configs simply set those attributes on the text field and allow the
41718 * native browser to provide those capabilities. For example, to enable auto complete and auto correct, simply
41719 * configure your text field like this:
41720 *
41721 * Ext.create('Ext.field.Text', {
41722 * label: 'Username',
41723 * autoComplete: true,
41724 * autoCorrect: true
41725 * });
41726 *
41727 * These configurations will be picked up by the native browser, which will enable the options at the OS level.
41728 *
41729 * Text field inherits from {@link Ext.field.Field}, which is the base class for all fields in Sencha Touch and provides
41730 * a lot of shared functionality for all fields, including setting values, clearing and basic validation. See the
41731 * {@link Ext.field.Field} documentation to see how to leverage its capabilities.
41732 *
41733 * For more information regarding forms and fields, please review [Using Forms in Sencha Touch Guide](../../../components/forms.html)
41734 */
41735 Ext.define('Ext.field.Text', {
41736 extend: Ext.field.Field ,
41737 xtype: 'textfield',
41738 alternateClassName: 'Ext.form.Text',
41739
41740 /**
41741 * @event focus
41742 * Fires when this field receives input focus
41743 * @param {Ext.field.Text} this This field
41744 * @param {Ext.event.Event} e
41745 */
41746
41747 /**
41748 * @event blur
41749 * Fires when this field loses input focus
41750 * @param {Ext.field.Text} this This field
41751 * @param {Ext.event.Event} e
41752 */
41753
41754 /**
41755 * @event paste
41756 * Fires when this field is pasted.
41757 * @param {Ext.field.Text} this This field
41758 * @param {Ext.event.Event} e
41759 */
41760
41761 /**
41762 * @event mousedown
41763 * Fires when this field receives a mousedown
41764 * @param {Ext.field.Text} this This field
41765 * @param {Ext.event.Event} e
41766 */
41767
41768 /**
41769 * @event keyup
41770 * @preventable doKeyUp
41771 * Fires when a key is released on the input element
41772 * @param {Ext.field.Text} this This field
41773 * @param {Ext.event.Event} e
41774 */
41775
41776 /**
41777 * @event clearicontap
41778 * @preventable doClearIconTap
41779 * Fires when the clear icon is tapped
41780 * @param {Ext.field.Text} this This field
41781 * @param {Ext.field.Input} input The field's input component.
41782 * @param {Ext.event.Event} e
41783 */
41784
41785 /**
41786 * @event change
41787 * Fires just before the field blurs if the field value has changed
41788 * @param {Ext.field.Text} this This field
41789 * @param {Mixed} newValue The new value
41790 * @param {Mixed} oldValue The original value
41791 */
41792
41793 /**
41794 * @event action
41795 * @preventable doAction
41796 * Fires whenever the return key or go is pressed. FormPanel listeners
41797 * for this event, and submits itself whenever it fires. Also note
41798 * that this event bubbles up to parent containers.
41799 * @param {Ext.field.Text} this This field
41800 * @param {Mixed} e The key event object
41801 */
41802
41803 config: {
41804 /**
41805 * @cfg
41806 * @inheritdoc
41807 */
41808 ui: 'text',
41809
41810 /**
41811 * @cfg
41812 * @inheritdoc
41813 */
41814 clearIcon: true,
41815
41816 /**
41817 * @cfg {String} placeHolder A string value displayed in the input (if supported) when the control is empty.
41818 * @accessor
41819 */
41820 placeHolder: null,
41821
41822 /**
41823 * @cfg {Number} maxLength The maximum number of permitted input characters.
41824 * @accessor
41825 */
41826 maxLength: null,
41827
41828 /**
41829 * True to set the field's DOM element autocomplete attribute to "on", false to set to "off".
41830 * @cfg {Boolean} autoComplete
41831 * @accessor
41832 */
41833 autoComplete: null,
41834
41835 /**
41836 * True to set the field's DOM element autocapitalize attribute to "on", false to set to "off".
41837 * @cfg {Boolean} autoCapitalize
41838 * @accessor
41839 */
41840 autoCapitalize: null,
41841
41842 /**
41843 * True to set the field DOM element autocorrect attribute to "on", false to set to "off".
41844 * @cfg {Boolean} autoCorrect
41845 * @accessor
41846 */
41847 autoCorrect: null,
41848
41849 /**
41850 * True to set the field DOM element readonly attribute to true.
41851 * @cfg {Boolean} readOnly
41852 * @accessor
41853 */
41854 readOnly: null,
41855
41856 /**
41857 * @cfg {Object} component The inner component for this field, which defaults to an input text.
41858 * @accessor
41859 */
41860 component: {
41861 xtype: 'input',
41862 type: 'text',
41863 fastFocus: true
41864 },
41865
41866 bubbleEvents: ['action']
41867 },
41868
41869 // @private
41870 initialize: function() {
41871 var me = this;
41872
41873 me.callParent();
41874
41875 me.getComponent().on({
41876 scope: this,
41877
41878 keyup : 'onKeyUp',
41879 change : 'onChange',
41880 focus : 'onFocus',
41881 blur : 'onBlur',
41882 paste : 'onPaste',
41883 mousedown : 'onMouseDown',
41884 clearicontap: 'onClearIconTap'
41885 });
41886
41887 // set the originalValue of the textfield, if one exists
41888 me.originalValue = me.getValue() || "";
41889 me.getComponent().originalValue = me.originalValue;
41890
41891 me.syncEmptyCls();
41892 },
41893
41894 syncEmptyCls: function() {
41895 var empty = (this._value) ? this._value.length : false,
41896 cls = Ext.baseCSSPrefix + 'empty';
41897
41898 if (empty) {
41899 this.removeCls(cls);
41900 } else {
41901 this.addCls(cls);
41902 }
41903 },
41904
41905 // @private
41906 updateValue: function(newValue) {
41907 var component = this.getComponent(),
41908 // allows newValue to be zero but not undefined or null (other falsey values)
41909 valueValid = newValue !== undefined && newValue !== null && newValue !== "";
41910
41911 if (component) {
41912 component.setValue(newValue);
41913 }
41914
41915 this[valueValid && this.isDirty() ? 'showClearIcon' : 'hideClearIcon']();
41916
41917 this.syncEmptyCls();
41918 },
41919
41920 getValue: function() {
41921 var me = this;
41922
41923 me._value = me.getComponent().getValue();
41924
41925 me.syncEmptyCls();
41926
41927 return me._value;
41928 },
41929
41930 // @private
41931 updatePlaceHolder: function(newPlaceHolder) {
41932 this.getComponent().setPlaceHolder(newPlaceHolder);
41933 },
41934
41935 // @private
41936 updateMaxLength: function(newMaxLength) {
41937 this.getComponent().setMaxLength(newMaxLength);
41938 },
41939
41940 // @private
41941 updateAutoComplete: function(newAutoComplete) {
41942 this.getComponent().setAutoComplete(newAutoComplete);
41943 },
41944
41945 // @private
41946 updateAutoCapitalize: function(newAutoCapitalize) {
41947 this.getComponent().setAutoCapitalize(newAutoCapitalize);
41948 },
41949
41950 // @private
41951 updateAutoCorrect: function(newAutoCorrect) {
41952 this.getComponent().setAutoCorrect(newAutoCorrect);
41953 },
41954
41955 // @private
41956 updateReadOnly: function(newReadOnly) {
41957 if (newReadOnly) {
41958 this.hideClearIcon();
41959 } else {
41960 this.showClearIcon();
41961 }
41962
41963 this.getComponent().setReadOnly(newReadOnly);
41964 },
41965
41966 // @private
41967 updateInputType: function(newInputType) {
41968 var component = this.getComponent();
41969 if (component) {
41970 component.setType(newInputType);
41971 }
41972 },
41973
41974 // @private
41975 updateName: function(newName) {
41976 var component = this.getComponent();
41977 if (component) {
41978 component.setName(newName);
41979 }
41980 },
41981
41982 // @private
41983 updateTabIndex: function(newTabIndex) {
41984 var component = this.getComponent();
41985 if (component) {
41986 component.setTabIndex(newTabIndex);
41987 }
41988 },
41989
41990 /**
41991 * Updates the {@link #inputCls} configuration on this fields {@link #component}
41992 * @private
41993 */
41994 updateInputCls: function(newInputCls, oldInputCls) {
41995 var component = this.getComponent();
41996 if (component) {
41997 component.replaceCls(oldInputCls, newInputCls);
41998 }
41999 },
42000
42001 doSetDisabled: function(disabled) {
42002 var me = this;
42003
42004 me.callParent(arguments);
42005
42006 var component = me.getComponent();
42007 if (component) {
42008 component.setDisabled(disabled);
42009 }
42010
42011 if (disabled) {
42012 me.hideClearIcon();
42013 } else {
42014 me.showClearIcon();
42015 }
42016 },
42017
42018 // @private
42019 showClearIcon: function() {
42020 var me = this,
42021 value = me.getValue(),
42022 // allows value to be zero but not undefined or null (other falsey values)
42023 valueValid = value !== undefined && value !== null && value !== "";
42024
42025 if (me.getClearIcon() && !me.getDisabled() && !me.getReadOnly() && valueValid) {
42026 me.element.addCls(Ext.baseCSSPrefix + 'field-clearable');
42027 }
42028
42029 return me;
42030 },
42031
42032 // @private
42033 hideClearIcon: function() {
42034 if (this.getClearIcon()) {
42035 this.element.removeCls(Ext.baseCSSPrefix + 'field-clearable');
42036 }
42037 },
42038
42039 onKeyUp: function(e) {
42040 this.fireAction('keyup', [this, e], 'doKeyUp');
42041 },
42042
42043 /**
42044 * Called when a key has been pressed in the `<input>`
42045 * @private
42046 */
42047 doKeyUp: function(me, e) {
42048 // getValue to ensure that we are in sync with the dom
42049 var value = me.getValue(),
42050 // allows value to be zero but not undefined or null (other falsey values)
42051 valueValid = value !== undefined && value !== null && value !== "";
42052
42053 this[valueValid ? 'showClearIcon' : 'hideClearIcon']();
42054
42055 if (e.browserEvent.keyCode === 13) {
42056 me.fireAction('action', [me, e], 'doAction');
42057 }
42058 },
42059
42060 doAction: function() {
42061 this.blur();
42062 },
42063
42064 onClearIconTap: function(input, e) {
42065 this.fireAction('clearicontap', [this, input, e], 'doClearIconTap');
42066 },
42067
42068 // @private
42069 doClearIconTap: function(me, e) {
42070 me.setValue('');
42071
42072 //sync with the input
42073 me.getValue();
42074 },
42075
42076 onChange: function(me, value, startValue) {
42077 me.fireEvent('change', this, value, startValue);
42078 },
42079
42080 onFocus: function(e) {
42081 this.addCls(Ext.baseCSSPrefix + 'field-focused');
42082 this.isFocused = true;
42083 this.fireEvent('focus', this, e);
42084 },
42085
42086 onBlur: function(e) {
42087 var me = this;
42088
42089 this.removeCls(Ext.baseCSSPrefix + 'field-focused');
42090 this.isFocused = false;
42091
42092 me.fireEvent('blur', me, e);
42093
42094 setTimeout(function() {
42095 me.isFocused = false;
42096 }, 50);
42097 },
42098
42099 onPaste: function(e) {
42100 this.fireEvent('paste', this, e);
42101 },
42102
42103 onMouseDown: function(e) {
42104 this.fireEvent('mousedown', this, e);
42105 },
42106
42107 /**
42108 * Attempts to set the field as the active input focus.
42109 * @return {Ext.field.Text} This field
42110 */
42111 focus: function() {
42112 this.getComponent().focus();
42113 return this;
42114 },
42115
42116 /**
42117 * Attempts to forcefully blur input focus for the field.
42118 * @return {Ext.field.Text} This field
42119 */
42120 blur: function() {
42121 this.getComponent().blur();
42122 return this;
42123 },
42124
42125 /**
42126 * Attempts to forcefully select all the contents of the input field.
42127 * @return {Ext.field.Text} this
42128 */
42129 select: function() {
42130 this.getComponent().select();
42131 return this;
42132 },
42133
42134 resetOriginalValue: function() {
42135 this.callParent();
42136 var component = this.getComponent();
42137 if(component && component.hasOwnProperty("originalValue")) {
42138 this.getComponent().originalValue = this.originalValue;
42139 }
42140 this.reset();
42141 },
42142
42143 reset: function() {
42144 this.getComponent().reset();
42145
42146 //we need to call this to sync the input with this field
42147 this.getValue();
42148
42149 this[this.isDirty() ? 'showClearIcon' : 'hideClearIcon']();
42150 },
42151
42152 isDirty: function() {
42153 var component = this.getComponent();
42154 if (component) {
42155 return component.isDirty();
42156 }
42157 return false;
42158 }
42159 });
42160
42161
42162 /**
42163 * @private
42164 */
42165 Ext.define('Ext.field.TextAreaInput', {
42166 extend: Ext.field.Input ,
42167 xtype : 'textareainput',
42168
42169 tag: 'textarea'
42170 });
42171
42172 /**
42173 * Creates an HTML textarea field on the page. This is useful whenever you need the user to enter large amounts of text
42174 * (i.e. more than a few words). Typically, text entry on mobile devices is not a pleasant experience for the user so
42175 * it's good to limit your use of text areas to only those occasions when free form text is required or alternative
42176 * input methods like select boxes or radio buttons are not possible. Text Areas are usually created inside forms, like
42177 * this:
42178 *
42179 * @example
42180 * Ext.create('Ext.form.Panel', {
42181 * fullscreen: true,
42182 * items: [
42183 * {
42184 * xtype: 'fieldset',
42185 * title: 'About you',
42186 * items: [
42187 * {
42188 * xtype: 'textfield',
42189 * label: 'Name',
42190 * name: 'name'
42191 * },
42192 * {
42193 * xtype: 'textareafield',
42194 * label: 'Bio',
42195 * maxRows: 4,
42196 * name: 'bio'
42197 * }
42198 * ]
42199 * }
42200 * ]
42201 * });
42202 *
42203 * In the example above we're creating a form with a {@link Ext.field.Text text field} for the user's name and a text
42204 * area for their bio. We used the {@link #maxRows} configuration on the text area to tell it to grow to a maximum of 4
42205 * rows of text before it starts using a scroll bar inside the text area to scroll the text.
42206 *
42207 * We can also create a text area outside the context of a form, like this:
42208 *
42209 * This creates two text fields inside a form. Text Fields can also be created outside of a Form, like this:
42210 *
42211 * Ext.create('Ext.field.TextArea', {
42212 * label: 'About You',
42213 * {@link #placeHolder}: 'Tell us about yourself...'
42214 * });
42215 *
42216 * For more information regarding forms and fields, please review [Using Forms in Sencha Touch Guide](../../../components/forms.html)
42217 */
42218 Ext.define('Ext.field.TextArea', {
42219 extend: Ext.field.Text ,
42220 xtype: 'textareafield',
42221
42222 alternateClassName: 'Ext.form.TextArea',
42223
42224 config: {
42225 /**
42226 * @cfg
42227 * @inheritdoc
42228 */
42229 ui: 'textarea',
42230
42231 /**
42232 * @cfg
42233 * @inheritdoc
42234 */
42235 autoCapitalize: false,
42236
42237 /**
42238 * @cfg
42239 * @inheritdoc
42240 */
42241 component: {
42242 xtype: 'textareainput'
42243 },
42244
42245 /**
42246 * @cfg {Number} maxRows The maximum number of lines made visible by the input.
42247 * @accessor
42248 */
42249 maxRows: null
42250 },
42251
42252 // @private
42253 updateMaxRows: function(newRows) {
42254 this.getComponent().setMaxRows(newRows);
42255 },
42256
42257 doSetHeight: function(newHeight) {
42258 this.callParent(arguments);
42259 var component = this.getComponent();
42260 component.input.setHeight(newHeight);
42261 },
42262
42263 doSetWidth: function(newWidth) {
42264 this.callParent(arguments);
42265 var component = this.getComponent();
42266 component.input.setWidth(newWidth);
42267 },
42268
42269 /**
42270 * Called when a key has been pressed in the `<input>`
42271 * @private
42272 */
42273 doKeyUp: function(me) {
42274 // getValue to ensure that we are in sync with the dom
42275 var value = me.getValue();
42276
42277 // show the {@link #clearIcon} if it is being used
42278 me[value ? 'showClearIcon' : 'hideClearIcon']();
42279 }
42280 });
42281
42282 /**
42283 * Utility class for generating different styles of message boxes. The framework provides a global singleton
42284 * {@link Ext.Msg} for common usage which you should use in most cases.
42285 *
42286 * If you want to use {@link Ext.MessageBox} directly, just think of it as a modal {@link Ext.Container}.
42287 *
42288 * Note that the MessageBox is asynchronous. Unlike a regular JavaScript `alert` (which will halt browser execution),
42289 * showing a MessageBox will not cause the code to stop. For this reason, if you have code that should only run _after_
42290 * some user feedback from the MessageBox, you must use a callback function (see the `fn` configuration option parameter
42291 * for the {@link #method-show show} method for more details).
42292 *
42293 * @example preview
42294 * Ext.Msg.alert('Title', 'The quick brown fox jumped over the lazy dog.', Ext.emptyFn);
42295 *
42296 * Checkout {@link Ext.Msg} for more examples.
42297 *
42298 */
42299 Ext.define('Ext.MessageBox', {
42300 extend : Ext.Sheet ,
42301
42302
42303
42304
42305
42306
42307
42308 config: {
42309 /**
42310 * @cfg
42311 * @inheritdoc
42312 */
42313 ui: 'dark',
42314
42315 /**
42316 * @cfg
42317 * @inheritdoc
42318 */
42319 baseCls: Ext.baseCSSPrefix + 'msgbox',
42320
42321 /**
42322 * @cfg {String} iconCls
42323 * CSS class for the icon. The icon should be 40px x 40px.
42324 * @accessor
42325 */
42326 iconCls: null,
42327
42328 /**
42329 * @cfg
42330 * @inheritdoc
42331 */
42332 showAnimation: {
42333 type: 'popIn',
42334 duration: 250,
42335 easing: 'ease-out'
42336 },
42337
42338 /**
42339 * @cfg
42340 * @inheritdoc
42341 */
42342 hideAnimation: {
42343 type: 'popOut',
42344 duration: 250,
42345 easing: 'ease-out'
42346 },
42347
42348 /**
42349 * Override the default `zIndex` so it is normally always above floating components.
42350 */
42351 zIndex: 999,
42352
42353 /**
42354 * @cfg {Number} defaultTextHeight
42355 * The default height in pixels of the message box's multiline textarea if displayed.
42356 * @accessor
42357 */
42358 defaultTextHeight: 75,
42359
42360 /**
42361 * @cfg {String} title
42362 * The title of this {@link Ext.MessageBox}.
42363 * @accessor
42364 */
42365 title: null,
42366
42367 /**
42368 * @cfg {Array/Object} buttons
42369 * An array of buttons, or an object of a button to be displayed in the toolbar of this {@link Ext.MessageBox}.
42370 */
42371 buttons: null,
42372
42373 /**
42374 * @cfg {String} message
42375 * The message to be displayed in the {@link Ext.MessageBox}.
42376 * @accessor
42377 */
42378 message: null,
42379
42380 /**
42381 * @cfg {String} msg
42382 * The message to be displayed in the {@link Ext.MessageBox}.
42383 * @removed 2.0.0 Please use {@link #message} instead.
42384 */
42385
42386 /**
42387 * @cfg {Object} prompt
42388 * The configuration to be passed if you want an {@link Ext.field.Text} or {@link Ext.field.TextArea} field
42389 * in your {@link Ext.MessageBox}.
42390 *
42391 * Pass an object with the property `multiLine` with a value of `true`, if you want the prompt to use a TextArea.
42392 *
42393 * Alternatively, you can just pass in an object which has an xtype/xclass of another component.
42394 *
42395 * prompt: {
42396 * xtype: 'textareafield',
42397 * value: 'test'
42398 * }
42399 *
42400 * @accessor
42401 */
42402 prompt: null,
42403
42404 /**
42405 * @private
42406 */
42407 modal: true,
42408
42409 /**
42410 * @cfg
42411 * @inheritdoc
42412 */
42413 layout: {
42414 type: 'vbox',
42415 pack: 'center'
42416 }
42417 },
42418
42419 platformConfig: [{
42420 theme: ['Windows'],
42421 ui: 'light',
42422 showAnimation: {
42423 type: 'fadeIn'
42424 },
42425 hideAnimation: {
42426 type: 'fadeOut'
42427 }
42428 }, {
42429 theme: ['Blackberry', 'Blackberry103'],
42430 ui: 'plain'
42431 }, {
42432 theme: ['MoutainView']
42433 }],
42434
42435 statics: {
42436 OK : {text: 'OK', itemId: 'ok', ui: 'action'},
42437 YES : {text: 'Yes', itemId: 'yes', ui: 'action'},
42438 NO : {text: 'No', itemId: 'no'},
42439 CANCEL: {text: 'Cancel', itemId: 'cancel'},
42440
42441 INFO : Ext.baseCSSPrefix + 'msgbox-info',
42442 WARNING : Ext.baseCSSPrefix + 'msgbox-warning',
42443 QUESTION: Ext.baseCSSPrefix + 'msgbox-question',
42444 ERROR : Ext.baseCSSPrefix + 'msgbox-error',
42445
42446 OKCANCEL: [
42447 {text: 'Cancel', itemId: 'cancel'},
42448 {text: 'OK', itemId: 'ok', ui : 'action'}
42449 ],
42450 YESNOCANCEL: [
42451 {text: 'Cancel', itemId: 'cancel'},
42452 {text: 'No', itemId: 'no'},
42453 {text: 'Yes', itemId: 'yes', ui: 'action'}
42454 ],
42455 YESNO: [
42456 {text: 'No', itemId: 'no'},
42457 {text: 'Yes', itemId: 'yes', ui: 'action'}
42458 ]
42459 },
42460
42461 constructor: function(config) {
42462 config = config || {};
42463
42464 if (config.hasOwnProperty('promptConfig')) {
42465 //<debug warn>
42466 Ext.Logger.deprecate("'promptConfig' config is deprecated, please use 'prompt' config instead", this);
42467 //</debug>
42468
42469 Ext.applyIf(config, {
42470 prompt: config.promptConfig
42471 });
42472
42473 delete config.promptConfig;
42474 }
42475
42476 if (config.hasOwnProperty('multiline') || config.hasOwnProperty('multiLine')) {
42477 config.prompt = config.prompt || {};
42478 Ext.applyIf(config.prompt, {
42479 multiLine: config.multiline || config.multiLine
42480 });
42481
42482 delete config.multiline;
42483 delete config.multiLine;
42484 }
42485
42486 this.defaultAllowedConfig = {};
42487 var allowedConfigs = ['ui', 'showAnimation', 'hideAnimation', 'title', 'message', 'prompt', 'iconCls', 'buttons', 'defaultTextHeight'],
42488 ln = allowedConfigs.length,
42489 i, allowedConfig;
42490
42491 for (i = 0; i < ln; i++) {
42492 allowedConfig = allowedConfigs[i];
42493 this.defaultAllowedConfig[allowedConfig] = this.defaultConfig[allowedConfig];
42494 }
42495
42496 this.callParent([config]);
42497 },
42498
42499 /**
42500 * Creates a new {@link Ext.Toolbar} instance using {@link Ext#factory}.
42501 * @private
42502 */
42503 applyTitle: function(config) {
42504 if (typeof config == "string") {
42505 config = {
42506 title: config
42507 };
42508 }
42509
42510 var minHeight = '1.3em';
42511 if (Ext.theme.is.Cupertino) {
42512 minHeight = '1.5em'
42513 } else if (Ext.filterPlatform('blackberry') || Ext.filterPlatform('ie10')) {
42514 minHeight = '2.6em';
42515 }
42516
42517 Ext.applyIf(config, {
42518 docked: 'top',
42519 minHeight: minHeight,
42520 ui: Ext.filterPlatform('blackberry') ? 'light' : 'dark',
42521 cls : this.getBaseCls() + '-title'
42522 });
42523
42524 if (Ext.theme.is.Tizen) {
42525 Ext.applyIf(config, {
42526 centered: false
42527 });
42528 }
42529
42530 return Ext.factory(config, Ext.Toolbar, this.getTitle());
42531 },
42532
42533 /**
42534 * Adds the new {@link Ext.Toolbar} instance into this container.
42535 * @private
42536 */
42537 updateTitle: function(newTitle) {
42538 if (newTitle) {
42539 this.add(newTitle);
42540 }
42541 },
42542
42543 /**
42544 * Adds the new {@link Ext.Toolbar} instance into this container.
42545 * @private
42546 */
42547 updateButtons: function(newButtons) {
42548 var me = this;
42549
42550 // If there are no new buttons or it is an empty array, set newButtons
42551 // to false
42552 newButtons = (!newButtons || newButtons.length === 0) ? false : newButtons;
42553
42554 if (newButtons) {
42555 if (me.buttonsToolbar) {
42556 me.buttonsToolbar.show();
42557 me.buttonsToolbar.removeAll();
42558 me.buttonsToolbar.setItems(newButtons);
42559 } else {
42560 var layout = {
42561 type: 'hbox',
42562 pack: 'center'
42563 };
42564
42565 var isFlexed = Ext.theme.is.CupertinoClassic || Ext.theme.is.MountainView || Ext.theme.is.Blackberry || Ext.theme.is.Blackberry103;
42566
42567 me.buttonsToolbar = Ext.create('Ext.Toolbar', {
42568 docked: 'bottom',
42569 defaultType: 'button',
42570 defaults: {
42571 flex: (isFlexed) ? 1 : undefined,
42572 ui: (Ext.theme.is.Blackberry || Ext.theme.is.Blackberry103) ? 'action' : undefined
42573 },
42574 layout: layout,
42575 ui: me.getUi(),
42576 cls: me.getBaseCls() + '-buttons',
42577 items: newButtons
42578 });
42579
42580 me.add(me.buttonsToolbar);
42581 }
42582 } else if (me.buttonsToolbar) {
42583 me.buttonsToolbar.hide();
42584 }
42585 },
42586
42587 /**
42588 * @private
42589 */
42590 applyMessage: function(config) {
42591 config = {
42592 html : config,
42593 cls : this.getBaseCls() + '-text'
42594 };
42595
42596 return Ext.factory(config, Ext.Component, this._message);
42597 },
42598
42599 /**
42600 * @private
42601 */
42602 updateMessage: function(newMessage) {
42603 if (newMessage) {
42604 this.add(newMessage);
42605 }
42606 },
42607
42608 getMessage: function() {
42609 if (this._message) {
42610 return this._message.getHtml();
42611 }
42612
42613 return null;
42614 },
42615
42616 /**
42617 * @private
42618 */
42619 applyIconCls: function(config) {
42620 config = {
42621 xtype : 'component',
42622 docked: 'left',
42623 width : 40,
42624 height: 40,
42625 baseCls: Ext.baseCSSPrefix + 'icon',
42626 hidden: (config) ? false : true,
42627 cls: config
42628 };
42629
42630 return Ext.factory(config, Ext.Component, this._iconCls);
42631 },
42632
42633 /**
42634 * @private
42635 */
42636 updateIconCls: function(newIconCls, oldIconCls) {
42637 //ensure the title and button elements are added first
42638 this.getTitle();
42639 this.getButtons();
42640
42641 if (newIconCls) {
42642 this.add(newIconCls);
42643 }
42644 else {
42645 this.remove(oldIconCls);
42646 }
42647 },
42648
42649 getIconCls: function() {
42650 var icon = this._iconCls,
42651 iconCls;
42652
42653 if (icon) {
42654 iconCls = icon.getCls();
42655 return (iconCls) ? iconCls[0] : null;
42656 }
42657
42658 return null;
42659 },
42660
42661 /**
42662 * @private
42663 */
42664 applyPrompt: function(prompt) {
42665 if (prompt) {
42666 var config = {
42667 label: false
42668 };
42669
42670 if (Ext.isObject(prompt)) {
42671 Ext.apply(config, prompt);
42672 }
42673
42674 if (config.multiLine) {
42675 config.height = Ext.isNumber(config.multiLine) ? parseFloat(config.multiLine) : this.getDefaultTextHeight();
42676 return Ext.factory(config, Ext.field.TextArea, this.getPrompt());
42677 } else {
42678 return Ext.factory(config, Ext.field.Text, this.getPrompt());
42679 }
42680 }
42681
42682 return prompt;
42683 },
42684
42685 /**
42686 * @private
42687 */
42688 updatePrompt: function(newPrompt, oldPrompt) {
42689 if (newPrompt) {
42690 this.add(newPrompt);
42691 }
42692
42693 if (oldPrompt) {
42694 this.remove(oldPrompt);
42695 }
42696 },
42697
42698 // @private
42699 // pass `fn` config to show method instead
42700 onClick: function(button) {
42701 if (button) {
42702 var config = button.config.userConfig || {},
42703 initialConfig = button.getInitialConfig(),
42704 prompt = this.getPrompt();
42705
42706 if (typeof config.fn == 'function') {
42707 button.disable();
42708 this.on({
42709 hiddenchange: function() {
42710 config.fn.call(
42711 config.scope || null,
42712 initialConfig.itemId || initialConfig.text,
42713 prompt ? prompt.getValue() : null,
42714 config
42715 );
42716 button.enable();
42717 },
42718 single: true,
42719 scope: this
42720 });
42721 }
42722
42723 if (config.input) {
42724 config.input.dom.blur();
42725 }
42726 }
42727
42728 this.hide();
42729 },
42730
42731 /**
42732 * Displays the {@link Ext.MessageBox} with a specified configuration. All
42733 * display functions (e.g. {@link #method-prompt}, {@link #alert}, {@link #confirm})
42734 * on MessageBox call this function internally, although those calls
42735 * are basic shortcuts and do not support all of the config options allowed here.
42736 *
42737 * Example usage:
42738 *
42739 * @example
42740 * Ext.Msg.show({
42741 * title: 'Address',
42742 * message: 'Please enter your address:',
42743 * width: 300,
42744 * buttons: Ext.MessageBox.OKCANCEL,
42745 * multiLine: true,
42746 * prompt : { maxlength : 180, autocapitalize : true },
42747 * fn: function(buttonId) {
42748 * alert('You pressed the "' + buttonId + '" button.');
42749 * }
42750 * });
42751 *
42752 * @param {Object} config An object with the following config options:
42753 *
42754 * @param {Object/Array} [config.buttons=false]
42755 * A button config object or Array of the same(e.g., `Ext.MessageBox.OKCANCEL` or `{text:'Foo', itemId:'cancel'}`),
42756 * or false to not show any buttons.
42757 *
42758 * @param {String} config.cls
42759 * A custom CSS class to apply to the message box's container element.
42760 *
42761 * @param {Function} config.fn
42762 * A callback function which is called when the dialog is dismissed by clicking on the configured buttons.
42763 *
42764 * @param {String} config.fn.buttonId The `itemId` of the button pressed, one of: 'ok', 'yes', 'no', 'cancel'.
42765 * @param {String} config.fn.value Value of the input field if either `prompt` or `multiline` option is `true`.
42766 * @param {Object} config.fn.opt The config object passed to show.
42767 *
42768 * @param {Number} [config.width=auto]
42769 * A fixed width for the MessageBox.
42770 *
42771 * @param {Number} [config.height=auto]
42772 * A fixed height for the MessageBox.
42773 *
42774 * @param {Object} config.scope
42775 * The scope of the callback function
42776 *
42777 * @param {String} config.icon
42778 * A CSS class that provides a background image to be used as the body icon for the dialog
42779 * (e.g. Ext.MessageBox.WARNING or 'custom-class').
42780 *
42781 * @param {Boolean} [config.modal=true]
42782 * `false` to allow user interaction with the page while the message box is displayed.
42783 *
42784 * @param {String} [config.message=&#160;]
42785 * A string that will replace the existing message box body text.
42786 * Defaults to the XHTML-compliant non-breaking space character `&#160;`.
42787 *
42788 * @param {Number} [config.defaultTextHeight=75]
42789 * The default height in pixels of the message box's multiline textarea if displayed.
42790 *
42791 * @param {Boolean} [config.prompt=false]
42792 * `true` to prompt the user to enter single-line text. Please view the {@link Ext.MessageBox#method-prompt} documentation in {@link Ext.MessageBox}.
42793 * for more information.
42794 *
42795 * @param {Boolean} [config.multiline=false]
42796 * `true` to prompt the user to enter multi-line text.
42797 *
42798 * @param {String} config.title
42799 * The title text.
42800 *
42801 * @param {String} config.value
42802 * The string value to set into the active textbox element if displayed.
42803 *
42804 * @return {Ext.MessageBox} this
42805 */
42806 show: function(initialConfig) {
42807 Ext.util.InputBlocker.blockInputs();
42808 //if it has not been added to a container, add it to the Viewport.
42809 if (!this.getParent() && Ext.Viewport) {
42810 Ext.Viewport.add(this);
42811 }
42812
42813 if (!initialConfig) {
42814 return this.callParent();
42815 }
42816
42817 var config = Ext.Object.merge({}, {
42818 value: ''
42819 }, initialConfig);
42820
42821 var buttons = initialConfig.buttons || Ext.MessageBox.OK || [],
42822 buttonBarItems = [],
42823 userConfig = initialConfig;
42824
42825 Ext.each(buttons, function(buttonConfig) {
42826 if (!buttonConfig) {
42827 return;
42828 }
42829
42830 buttonBarItems.push(Ext.apply({
42831 userConfig: userConfig,
42832 scope : this,
42833 handler : 'onClick'
42834 }, buttonConfig));
42835 }, this);
42836
42837 config.buttons = buttonBarItems;
42838
42839 if (config.promptConfig) {
42840 //<debug warn>
42841 Ext.Logger.deprecate("'promptConfig' config is deprecated, please use 'prompt' config instead", this);
42842 //</debug>
42843 }
42844 config.prompt = (config.promptConfig || config.prompt) || null;
42845
42846 if (config.multiLine) {
42847 config.prompt = config.prompt || {};
42848 config.prompt.multiLine = config.multiLine;
42849 delete config.multiLine;
42850 }
42851
42852 config = Ext.merge({}, this.defaultAllowedConfig, config);
42853
42854 this.setConfig(config);
42855
42856 var prompt = this.getPrompt();
42857 if (prompt) {
42858 prompt.setValue(initialConfig.value || '');
42859 }
42860
42861 this.callParent();
42862
42863 return this;
42864 },
42865
42866 /**
42867 * Displays a standard read-only message box with an OK button (comparable to the basic JavaScript alert prompt). If
42868 * a callback function is passed it will be called after the user clicks the button, and the `itemId` of the button
42869 * that was clicked will be passed as the only parameter to the callback.
42870 *
42871 * @param {String} title The title bar text.
42872 * @param {String} message The message box body text.
42873 * @param {Function} [fn] A callback function which is called when the dialog is dismissed by clicking on the configured buttons.
42874 * @param {String} fn.buttonId The `itemId` of the button pressed, one of: 'ok', 'yes', 'no', 'cancel'.
42875 * @param {String} fn.value Value of the input field if either `prompt` or `multiLine` option is `true`.
42876 * @param {Object} fn.opt The config object passed to show.
42877 * @param {Object} [scope] The scope (`this` reference) in which the callback is executed.
42878 * Defaults to: the browser window
42879 *
42880 * @return {Ext.MessageBox} this
42881 */
42882 alert: function(title, message, fn, scope) {
42883 return this.show({
42884 title: title || null,
42885 message: message || null,
42886 buttons: Ext.MessageBox.OK,
42887 promptConfig: false,
42888 fn: function() {
42889 if (fn) {
42890 fn.apply(scope, arguments);
42891 }
42892 },
42893 scope: scope
42894 });
42895 },
42896
42897 /**
42898 * Displays a confirmation message box with Yes and No buttons (comparable to JavaScript's confirm). If a callback
42899 * function is passed it will be called after the user clicks either button, and the id of the button that was
42900 * clicked will be passed as the only parameter to the callback (could also be the top-right close button).
42901 *
42902 * @param {String} title The title bar text.
42903 * @param {String} message The message box body text.
42904 * @param {Function} fn A callback function which is called when the dialog is dismissed by clicking on the configured buttons.
42905 * @param {String} fn.buttonId The `itemId` of the button pressed, one of: 'ok', 'yes', 'no', 'cancel'.
42906 * @param {String} fn.value Value of the input field if either `prompt` or `multiLine` option is `true`.
42907 * @param {Object} fn.opt The config object passed to show.
42908 * @param {Object} [scope] The scope (`this` reference) in which the callback is executed.
42909 *
42910 * Defaults to: the browser window
42911 *
42912 * @return {Ext.MessageBox} this
42913 */
42914 confirm: function(title, message, fn, scope) {
42915 return this.show({
42916 title : title || null,
42917 message : message || null,
42918 buttons : Ext.MessageBox.YESNO,
42919 promptConfig: false,
42920 scope : scope,
42921 fn: function() {
42922 if (fn) {
42923 fn.apply(scope, arguments);
42924 }
42925 }
42926 });
42927 },
42928
42929 /**
42930 * Displays a message box with OK and Cancel buttons prompting the user to enter some text (comparable to
42931 * JavaScript's prompt). The prompt can be a single-line or multi-line textbox. If a callback function is passed it
42932 * will be called after the user clicks either button, and the id of the button that was clicked (could also be the
42933 * top-right close button) and the text that was entered will be passed as the two parameters to the callback.
42934 *
42935 * Example usage:
42936 *
42937 * @example
42938 * Ext.Msg.prompt(
42939 * 'Welcome!',
42940 * 'What\'s your name going to be today?',
42941 * function (buttonId, value) {
42942 * console.log(value);
42943 * },
42944 * null,
42945 * false,
42946 * null,
42947 * {
42948 * autoCapitalize: true,
42949 * placeHolder: 'First-name please...'
42950 * }
42951 * );
42952 *
42953 * @param {String} title The title bar text.
42954 * @param {String} message The message box body text.
42955 * @param {Function} fn A callback function which is called when the dialog is dismissed by clicking on the configured buttons.
42956 * @param {String} fn.buttonId The `itemId` of the button pressed, one of: 'ok', 'yes', 'no', 'cancel'.
42957 * @param {String} fn.value Value of the input field if either `prompt` or `multiLine` option is `true`.
42958 * @param {Object} fn.opt The config object passed to show.
42959 * @param {Object} scope The scope (`this` reference) in which the callback is executed.
42960 *
42961 * Defaults to: the browser window.
42962 *
42963 * @param {Boolean/Number} [multiLine=false] `true` to create a multiline textbox using the `defaultTextHeight` property,
42964 * or the height in pixels to create the textbox.
42965 *
42966 * @param {String} [value] Default value of the text input element.
42967 *
42968 * @param {Object} [prompt=true]
42969 * The configuration for the prompt. See the {@link Ext.MessageBox#cfg-prompt prompt} documentation in {@link Ext.MessageBox}
42970 * for more information.
42971 *
42972 * @return {Ext.MessageBox} this
42973 */
42974 prompt: function(title, message, fn, scope, multiLine, value, prompt) {
42975 return this.show({
42976 title : title || null,
42977 message : message || null,
42978 buttons : Ext.MessageBox.OKCANCEL,
42979 scope : scope,
42980 prompt : prompt || true,
42981 multiLine: multiLine,
42982 value : value,
42983 fn: function() {
42984 if (fn) {
42985 fn.apply(scope, arguments);
42986 }
42987 }
42988 });
42989 }
42990 }, function(MessageBox) {
42991
42992 Ext.onSetup(function() {
42993 /**
42994 * @class Ext.Msg
42995 * @extends Ext.MessageBox
42996 * @singleton
42997 *
42998 * A global shared singleton instance of the {@link Ext.MessageBox} class.
42999 *
43000 * Allows for simple creation of various different alerts and notifications.
43001 *
43002 * To change any configurations on this singleton instance, you must change the
43003 * `defaultAllowedConfig` object. For example to remove all animations on `Msg`:
43004 *
43005 * Ext.Msg.defaultAllowedConfig.showAnimation = false;
43006 * Ext.Msg.defaultAllowedConfig.hideAnimation = false;
43007 *
43008 * ## Examples
43009 *
43010 * ### Alert
43011 * Use the {@link #alert} method to show a basic alert:
43012 *
43013 * @example preview
43014 * Ext.Msg.alert('Title', 'The quick brown fox jumped over the lazy dog.', Ext.emptyFn);
43015 *
43016 * ### Prompt
43017 * Use the {@link #method-prompt} method to show an alert which has a textfield:
43018 *
43019 * @example preview
43020 * Ext.Msg.prompt('Name', 'Please enter your name:', function(text) {
43021 * // process text value and close...
43022 * });
43023 *
43024 * ### Confirm
43025 * Use the {@link #confirm} method to show a confirmation alert (shows yes and no buttons).
43026 *
43027 * @example preview
43028 * Ext.Msg.confirm("Confirmation", "Are you sure you want to do that?", Ext.emptyFn);
43029 */
43030 Ext.Msg = new MessageBox;
43031 });
43032 });
43033
43034
43035 /**
43036 * A Traversable mixin.
43037 * @private
43038 */
43039 Ext.define('Ext.mixin.Progressable', {
43040 extend: Ext.mixin.Mixin ,
43041 isProgressable: true,
43042
43043 mixinConfig: {
43044 id: 'progressable'
43045 },
43046
43047 config: {
43048
43049 /**
43050 * @cfg {Number} minProgressInput
43051 * Minimum input value for this indicator
43052 */
43053 minProgressInput: 0,
43054
43055 /**
43056 * @cfg {Number} maxProgressInput
43057 * Maximum input value for this indicator
43058 */
43059 maxProgressInput: 1,
43060
43061 /**
43062 * @cfg {Number} minProgressOutput
43063 * Minimum output value for this indicator
43064 */
43065 minProgressOutput: 0,
43066
43067 /**
43068 * @cfg {Number} maxProgressOutput
43069 * Maximum output value for this indicator
43070 */
43071 maxProgressOutput: 100,
43072
43073 /**
43074 * @cfg {Boolean} dynamic
43075 *
43076 * When false this indicator will only receive progressStart and progressEnd commands, no progressUpdate commands will be sent.
43077 *
43078 */
43079 dynamic: true,
43080
43081 /**
43082 * @cfg {String} state
43083 *
43084 * Current state of the progressIndicator. Should be used for switching progress states like download to upload.
43085 */
43086 state: null
43087 },
43088
43089 // @private
43090 _progressActive: false,
43091 _progress: 0,
43092 _rawProgress: 0,
43093
43094 onStartProgress: Ext.emptyFn,
43095 onUpdateProgress: Ext.emptyFn,
43096 onEndProgress: Ext.emptyFn,
43097
43098 startProgress: function() {
43099 if (!this._progressActive) {
43100 this._progressActive = true;
43101 this.onStartProgress();
43102 this.updateProgress(this.getMinProgressInput());
43103 }
43104 },
43105
43106 updateProgress: function(value, state) {
43107 if(state && state != this.getState()) this.setState(state);
43108 if(value > this.getMaxProgressInput()) value = this.getMaxProgressInput();
43109 if(value < this.getMinProgressInput()) value = this.getMinProgressInput();
43110
43111 var mappedValue = this.mapValues(value, this.getMinProgressInput(), this.getMaxProgressInput(), this.getMinProgressOutput(), this.getMaxProgressOutput());
43112 this._progress = mappedValue;
43113 this._rawProgress = value;
43114
43115 if(this.getDynamic()) {
43116 this.onUpdateProgress(mappedValue);
43117 }
43118 },
43119
43120 endProgress: function() {
43121 if (this._progressActive) {
43122 this._progressActive = false;
43123 this.updateProgress(this.getMaxProgressInput());
43124 this.onEndProgress()
43125 }
43126 },
43127
43128 mapValues: function(value, inputMin, inputMax, outputMin, outputMax) {
43129 return (value - inputMin) / (inputMax - inputMin) * (outputMax - outputMin) + outputMin;
43130 },
43131
43132 setProgress: function(value) {
43133 this.updateProgress(value);
43134 },
43135
43136 getProgress: function() {
43137 return this._progress
43138 },
43139
43140 getRawProgress: function() {
43141 return this._rawProgress;
43142 }
43143 });
43144
43145 /**
43146 * {@link Ext.ProgressIndicator} provides a progress indicator for file uploads.
43147 */
43148 Ext.define('Ext.ProgressIndicator', {
43149 extend: Ext.Container ,
43150
43151 mixins: [ Ext.mixin.Progressable ],
43152
43153 xtype: 'progressindicator',
43154
43155 config: {
43156 baseCls: Ext.baseCSSPrefix + 'progressindicator',
43157 hidden: true,
43158 modal: true,
43159 centered: true,
43160
43161 /**
43162 * @cfg {String/Ext.XTemplate/Object} loadingText
43163 * This template is used when progress is dynamic (many updates will be received). Template will be passed
43164 * and object with properties percent and state.
43165 *
43166 * If a String or XTemplate is given that text will be used for all states of loading. One can optionally pass in an object
43167 * with the properties 'upload' and/or 'download' with custom state templates.
43168 *
43169 *
43170 * @accessor
43171 */
43172 loadingText: {
43173 any: 'Loading: {percent}%',
43174 upload: 'Uploading: {percent}%',
43175 download: 'Downloading: {percent}%'
43176 },
43177
43178 /**
43179 * @cfg {String/Object} fallbackText
43180 * This String is used when progress is not dynamic (only start and end events will be received).
43181 *
43182 * If a String is given that text will be used for all states of loading. One can optionally pass in an object
43183 * with the properties 'upload' and/or 'download' with custom state strings.
43184 *
43185 * @accessor
43186 */
43187 fallbackText: {
43188 any: 'Loading',
43189 upload: 'Uploading',
43190 download: 'Downloading'
43191 },
43192
43193 /**
43194 * @cfg {Object} monitoredStates
43195 * Object with the properties of 'upload' and 'download'. To disable progress monitoring of any state simply set
43196 * it to false. For example:
43197 *
43198 * monitoredStates: {
43199 * upload:false
43200 * }
43201 *
43202 * @accessor
43203 */
43204 monitoredStates: {
43205 upload: true,
43206 download: true
43207 },
43208
43209 showAnimation: !Ext.browser.is.AndroidStock ? {
43210 type: 'slideIn',
43211 direction: "left",
43212 duration: 250,
43213 easing: 'ease-out'
43214 } : null,
43215
43216 hideAnimation: !Ext.browser.is.AndroidStock ? {
43217 type: 'slideOut',
43218 direction: "left",
43219 duration: 250,
43220 easing: 'ease-in'
43221 } : null,
43222
43223 // @private
43224 minProgressOutput: 0,
43225 // @private
43226 maxProgressOutput: 1,
43227 //@private
43228 state: null
43229 },
43230
43231 constructor: function() {
43232 this.emptyTpl = new Ext.XTemplate("");
43233 this.callParent(arguments);
43234 },
43235
43236 getElementConfig: function() {
43237 return {
43238 reference: 'element',
43239 classList: ['x-container', 'x-unsized'],
43240 children: [
43241 {
43242 reference: 'innerElement',
43243 className: Ext.baseCSSPrefix + 'progressindicator-inner',
43244 children: [
43245 {
43246 reference: 'progressBarText',
43247 className: Ext.baseCSSPrefix + 'progressindicator-text'
43248 },
43249 {
43250 reference: 'progressBar',
43251 className: Ext.baseCSSPrefix + 'progressindicator-bar',
43252 children: [
43253 {
43254 reference: 'progressBarFill',
43255 className: Ext.baseCSSPrefix + 'progressindicator-bar-fill'
43256 }
43257 ]
43258 }
43259 ]
43260 }
43261 ]
43262 };
43263 },
43264
43265 onStartProgress: function() {
43266 if (!this.getParent()) {
43267 Ext.Viewport.add(this);
43268 }
43269 this.show();
43270 },
43271
43272 onEndProgress: function() {
43273 this.hide();
43274 },
43275
43276 onUpdateProgress: function() {
43277 this.updateBar();
43278 },
43279
43280 getLoadingText: function() {
43281 var state = this.getState();
43282 if (this._loadingText[state]) {
43283 return this._loadingText[state];
43284 }
43285
43286 if (this._loadingText["any"]) {
43287 return this._loadingText["any"];
43288 }
43289
43290 return this.emptyTpl;
43291 },
43292
43293 applyLoadingText: function(loadingText) {
43294 var tpl = {}, property, value;
43295 if (Ext.isString(loadingText)) {
43296 tpl = {
43297 any: new Ext.XTemplate(loadingText)
43298 }
43299 } else if (loadingText instanceof Ext.XTemplate) {
43300 tpl = {
43301 any: loadingText
43302 }
43303 } else {
43304 for (property in loadingText) {
43305 value = loadingText[property];
43306 tpl[property] = new Ext.XTemplate(value);
43307 }
43308 }
43309 if (!tpl.any) {
43310 tpl.any = this.emptyTpl;
43311 }
43312 return tpl;
43313 },
43314
43315 getFallbackText: function() {
43316 var state = this.getState();
43317 if (this._fallbackText[state]) {
43318 return this._fallbackText[state];
43319 }
43320
43321 if(this._fallbackText["any"]) {
43322 return this._fallbackText["any"];
43323 }
43324
43325 return "";
43326 },
43327
43328 applyFallbackText: function(fallbackText) {
43329 var obj = {}, property, value;
43330 if (Ext.isString(fallbackText)) {
43331 obj = {
43332 any: fallbackText
43333 }
43334 } else {
43335 for (property in fallbackText) {
43336 value = fallbackText[property];
43337 obj[property] = value;
43338 }
43339 }
43340 if (!obj.any) {
43341 obj.any = this.emptyTpl;
43342 }
43343 return obj;
43344 },
43345
43346 updateDynamic: function(value) {
43347 if (!value) {
43348 this.progressBarText.setHtml(this.getFallbackText());
43349 this.progressBar.setWidth("100%");
43350 } else {
43351 this.updateBar();
43352 }
43353 return value;
43354 },
43355
43356 updateBar: function() {
43357 var state = this.getState();
43358 if(this.getMonitoredStates()[state] !== true) {
43359 this.progressBarText.setHtml(this.getFallbackText());
43360 this.progressBar.setWidth("100%");
43361 return;
43362 }
43363
43364 var percent = this.getProgress() * 100;
43365 if (!Ext.isNumber(percent)) percent = 0;
43366 this.progressBar.setWidth(percent + "%");
43367
43368 var loadingText = this.getLoadingText();
43369 if (loadingText) {
43370 this.progressBarText.setHtml(this.getLoadingText().apply({state:state, percent: Math.ceil(percent) || 0}));
43371 } else {
43372 this.progressBarText.setHtml('');
43373 }
43374 }
43375 })
43376 ;
43377
43378 /**
43379 * @private
43380 */
43381 Ext.define('Ext.Promise', {
43382 statics: {
43383 when: function() {
43384 var ret = new this,
43385 promises = Array.prototype.slice.call(arguments),
43386 index = -1,
43387 results = [],
43388 promise;
43389
43390 function onRejected(e) {
43391 ret.reject(e);
43392 }
43393
43394 function onFulfilled(result) {
43395 promise = promises.shift();
43396
43397 if (index >= 0) {
43398 results[index] = result;
43399 }
43400
43401 index++;
43402
43403 if (promise) {
43404 promise.then(onFulfilled, onRejected);
43405 }
43406 else {
43407 ret.fulfill.apply(ret, results);
43408 }
43409 }
43410
43411 onFulfilled();
43412
43413 return ret;
43414 },
43415
43416 whenComplete: function(promises) {
43417 var ret = new this,
43418 index = -1,
43419 fulfilledResults = [],
43420 rejectedReasons = [],
43421 promise;
43422
43423 function onRejected(reason) {
43424 promise = promises.shift();
43425 rejectedReasons.push(reason);
43426 next(promise);
43427 }
43428
43429 function onFulfilled(result) {
43430 promise = promises.shift();
43431 fulfilledResults.push(result);
43432 next(promise);
43433 }
43434
43435 function next(promise) {
43436 index++;
43437
43438 if (promise) {
43439 promise.then(onFulfilled, onRejected);
43440 }
43441 else {
43442 ret.fulfill.call(ret, {
43443 fulfilled: fulfilledResults,
43444 rejected: rejectedReasons
43445 });
43446 }
43447 }
43448
43449 next(promises.shift());
43450
43451 return ret;
43452 },
43453
43454 from: function() {
43455 var promise = new this;
43456 promise.completed = 1;
43457 promise.lastResults = arguments;
43458 return promise;
43459 },
43460
43461 fail: function(reason) {
43462 var promise = new this;
43463 promise.completed = -1;
43464 promise.lastReason = reason;
43465 return promise;
43466 }
43467 },
43468
43469 completed: 0,
43470
43471 getListeners: function(init) {
43472 var listeners = this.listeners;
43473
43474 if (!listeners && init) {
43475 this.listeners = listeners = [];
43476 }
43477
43478 return listeners;
43479 },
43480
43481 then: function(scope, success, error) {
43482 if (typeof scope == 'function') {
43483 error = success;
43484 success = scope;
43485 scope = null;
43486 }
43487
43488 if (typeof success == 'string') {
43489 success = scope[success];
43490 }
43491
43492 if (typeof error == 'string') {
43493 error = scope[error];
43494 }
43495
43496 return this.doThen(scope, success, error);
43497 },
43498
43499 doThen: function(scope, success, error) {
43500 var Promise = Ext.Promise,
43501 completed = this.completed,
43502 promise, result;
43503
43504 if (completed === -1) {
43505 if (error) {
43506 error.call(scope, this.lastReason);
43507 }
43508 return this;
43509 }
43510
43511 if (completed === 1 && !this.isFulfilling) {
43512 if (!success) {
43513 return this;
43514 }
43515
43516 result = success.apply(scope, this.lastResults);
43517
43518 if (result instanceof Promise) {
43519 promise = result;
43520 }
43521 else {
43522 promise = Promise.from(result);
43523 }
43524 }
43525 else {
43526 promise = new Promise;
43527 promise.$owner = this;
43528
43529 this.getListeners(true).push({
43530 scope: scope,
43531 success: success,
43532 error: error,
43533 promise: promise
43534 });
43535 }
43536
43537 return promise;
43538 },
43539
43540 error: function(scope, error) {
43541 if (typeof scope == 'function') {
43542 error = scope;
43543 scope = null;
43544 }
43545
43546 if (typeof error == 'string') {
43547 error = scope[error];
43548 }
43549
43550 return this.doThen(scope, null, error);
43551 },
43552
43553 fulfill: function() {
43554 var results = arguments,
43555 listeners, listener, scope, success, promise, callbackResults;
43556
43557 this.lastResults = results;
43558 this.completed = 1;
43559
43560 while (listeners = this.getListeners()) {
43561 delete this.listeners;
43562 this.isFulfilling = true;
43563
43564 while (listener = listeners.shift()) {
43565 success = listener.success;
43566 scope = listener.scope;
43567 promise = listener.promise;
43568 delete promise.$owner;
43569
43570 if (success) {
43571 callbackResults = success.apply(scope, results);
43572
43573 if (callbackResults instanceof Ext.Promise) {
43574 callbackResults.connect(promise);
43575 }
43576 else {
43577 promise.fulfill(callbackResults);
43578 }
43579 }
43580 else {
43581 promise.fulfill(results);
43582 }
43583 }
43584
43585 this.isFulfilling = false;
43586 }
43587
43588 return this;
43589 },
43590
43591 connect: function(promise) {
43592 var me = this;
43593
43594 me.then(promise, function(result) {
43595 this.fulfill(result);
43596 return result;
43597 }, 'reject');
43598 },
43599
43600 reject: function(reason) {
43601 var listeners = this.getListeners(),
43602 listener, error, promise;
43603
43604 this.lastReason = reason;
43605 this.completed = -1;
43606
43607 if (listeners) {
43608 delete this.listeners;
43609 while (listener = listeners.shift()) {
43610 error = listener.error;
43611 promise = listener.promise;
43612 delete promise.$owner;
43613
43614 if (error) {
43615 error.call(listener.scope, reason);
43616 }
43617
43618 promise.reject(reason);
43619 }
43620 }
43621
43622 return this;
43623 },
43624
43625 cancel: function() {
43626 var listeners = this.getListeners(),
43627 owner = this.$owner,
43628 i, ln, listener;
43629
43630 if (listeners) {
43631 for (i = 0, ln = listeners.length; i < ln; i++) {
43632 listener = listeners[i];
43633 listener.promise.cancel();
43634 }
43635 listeners.length = 0;
43636 delete this.listeners;
43637 }
43638
43639 if (owner) {
43640 delete this.$owner;
43641 owner.cancel();
43642 }
43643 }
43644 });
43645
43646 /**
43647 * SegmentedButton is a container for a group of {@link Ext.Button}s. Generally a SegmentedButton would be
43648 * a child of a {@link Ext.Toolbar} and would be used to switch between different views.
43649 *
43650 * ## Example usage:
43651 *
43652 * @example
43653 * var segmentedButton = Ext.create('Ext.SegmentedButton', {
43654 * allowMultiple: true,
43655 * items: [
43656 * {
43657 * text: 'Option 1'
43658 * },
43659 * {
43660 * text: 'Option 2',
43661 * pressed: true
43662 * },
43663 * {
43664 * text: 'Option 3'
43665 * }
43666 * ],
43667 * listeners: {
43668 * toggle: function(container, button, pressed){
43669 * alert("User toggled the '" + button.getText() + "' button: " + (pressed ? 'on' : 'off'));
43670 * }
43671 * }
43672 * });
43673 * Ext.Viewport.add({ xtype: 'container', padding: 10, items: [segmentedButton] });
43674 */
43675 Ext.define('Ext.SegmentedButton', {
43676 extend: Ext.Container ,
43677 xtype : 'segmentedbutton',
43678
43679
43680 config: {
43681 /**
43682 * @cfg
43683 * @inheritdoc
43684 */
43685 baseCls: Ext.baseCSSPrefix + 'segmentedbutton',
43686
43687 /**
43688 * @cfg {String} pressedCls
43689 * CSS class when a button is in pressed state.
43690 * @accessor
43691 */
43692 pressedCls: Ext.baseCSSPrefix + 'button-pressed',
43693
43694 /**
43695 * @cfg {Boolean} allowMultiple
43696 * Allow multiple pressed buttons.
43697 * @accessor
43698 */
43699 allowMultiple: false,
43700
43701 /**
43702 * @cfg {Boolean} allowDepress
43703 * Allow toggling the pressed state of each button.
43704 * Defaults to `true` when {@link #allowMultiple} is `true`.
43705 * @accessor
43706 */
43707 allowDepress: false,
43708
43709 /**
43710 * @cfg {Boolean} allowToggle Allow child buttons to be pressed when tapped on. Set to `false` to allow tapping but not toggling of the buttons.
43711 * @accessor
43712 */
43713 allowToggle: true,
43714
43715 /**
43716 * @cfg {Array} pressedButtons
43717 * The pressed buttons for this segmented button.
43718 *
43719 * You can remove all pressed buttons by calling {@link #setPressedButtons} with an empty array.
43720 * @accessor
43721 */
43722 pressedButtons: [],
43723
43724 /**
43725 * @cfg
43726 * @inheritdoc
43727 */
43728 layout: {
43729 type : 'hbox',
43730 align: 'stretch'
43731 },
43732
43733 /**
43734 * @cfg
43735 * @inheritdoc
43736 */
43737 defaultType: 'button'
43738 },
43739
43740 /**
43741 * @event toggle
43742 * Fires when any child button's pressed state has changed.
43743 * @param {Ext.SegmentedButton} this
43744 * @param {Ext.Button} button The toggled button.
43745 * @param {Boolean} isPressed Boolean to indicate if the button was pressed or not.
43746 */
43747
43748 initialize: function() {
43749 var me = this;
43750
43751 me.callParent();
43752
43753 me.on({
43754 delegate: '> button',
43755 scope : me,
43756 tap: 'onButtonRelease'
43757 });
43758
43759 me.onAfter({
43760 delegate: '> button',
43761 scope : me,
43762 hide: 'onButtonHiddenChange',
43763 show: 'onButtonHiddenChange'
43764 });
43765 },
43766
43767 updateAllowMultiple: function(allowMultiple) {
43768 if (!this.initialized && !this.getInitialConfig().hasOwnProperty('allowDepress') && allowMultiple) {
43769 this.setAllowDepress(true);
43770 }
43771 },
43772
43773 /**
43774 * We override `initItems` so we can check for the pressed config.
43775 */
43776 applyItems: function() {
43777 var me = this,
43778 pressedButtons = [],
43779 ln, i, item, items;
43780
43781 //call the parent first so the items get converted into a MixedCollection
43782 me.callParent(arguments);
43783
43784 items = this.getItems();
43785 ln = items.length;
43786
43787 for (i = 0; i < ln; i++) {
43788 item = items.items[i];
43789 if (item.getInitialConfig('pressed')) {
43790 pressedButtons.push(items.items[i]);
43791 }
43792 }
43793
43794 me.updateFirstAndLastCls(items);
43795
43796 me.setPressedButtons(pressedButtons);
43797 },
43798
43799 /**
43800 * Button sets a timeout of 10ms to remove the {@link #pressedCls} on the release event.
43801 * We don't want this to happen, so lets return `false` and cancel the event.
43802 * @private
43803 */
43804 onButtonRelease: function(button) {
43805 if (!this.getAllowToggle()) {
43806 return;
43807 }
43808 var me = this,
43809 pressedButtons = me.getPressedButtons() || [],
43810 buttons = [],
43811 alreadyPressed;
43812
43813 if (!me.getDisabled() && !button.getDisabled()) {
43814 //if we allow for multiple pressed buttons, use the existing pressed buttons
43815 if (me.getAllowMultiple()) {
43816 buttons = pressedButtons.concat(buttons);
43817 }
43818
43819 alreadyPressed = (buttons.indexOf(button) !== -1) || (pressedButtons.indexOf(button) !== -1);
43820
43821 //if we allow for depressing buttons, and the new pressed button is currently pressed, remove it
43822 if (alreadyPressed && me.getAllowDepress()) {
43823 Ext.Array.remove(buttons, button);
43824 } else if (!alreadyPressed || !me.getAllowDepress()) {
43825 buttons.push(button);
43826 }
43827
43828 me.setPressedButtons(buttons);
43829 }
43830 },
43831
43832 onItemAdd: function() {
43833 this.callParent(arguments);
43834 this.updateFirstAndLastCls(this.getItems());
43835 },
43836
43837 onItemRemove: function() {
43838 this.callParent(arguments);
43839 this.updateFirstAndLastCls(this.getItems());
43840 },
43841
43842 // @private
43843 onButtonHiddenChange: function() {
43844 this.updateFirstAndLastCls(this.getItems());
43845 },
43846
43847 // @private
43848 updateFirstAndLastCls: function(items) {
43849 var ln = items.length,
43850 basePrefix = Ext.baseCSSPrefix,
43851 firstCls = basePrefix + 'first',
43852 lastCls = basePrefix + 'last',
43853 item, i;
43854
43855 //remove all existing classes
43856 for (i = 0; i < ln; i++) {
43857 item = items.items[i];
43858 item.removeCls(firstCls);
43859 item.removeCls(lastCls);
43860 }
43861
43862 //add a first cls to the first non-hidden button
43863 for (i = 0; i < ln; i++) {
43864 item = items.items[i];
43865 if (!item.isHidden()) {
43866 item.addCls(firstCls);
43867 break;
43868 }
43869 }
43870
43871 //add a last cls to the last non-hidden button
43872 for (i = ln - 1; i >= 0; i--) {
43873 item = items.items[i];
43874 if (!item.isHidden()) {
43875 item.addCls(lastCls);
43876 break;
43877 }
43878 }
43879 },
43880
43881 /**
43882 * @private
43883 */
43884 applyPressedButtons: function(newButtons) {
43885 var me = this,
43886 array = [],
43887 button, ln, i;
43888
43889 if (me.getAllowToggle()) {
43890 if (Ext.isArray(newButtons)) {
43891 ln = newButtons.length;
43892 for (i = 0; i< ln; i++) {
43893 button = me.getComponent(newButtons[i]);
43894 if (button && array.indexOf(button) === -1) {
43895 array.push(button);
43896 }
43897 }
43898 } else {
43899 button = me.getComponent(newButtons);
43900 if (button && array.indexOf(button) === -1) {
43901 array.push(button);
43902 }
43903 }
43904 }
43905
43906 return array;
43907 },
43908
43909 /**
43910 * Updates the pressed buttons.
43911 * @private
43912 */
43913 updatePressedButtons: function(newButtons, oldButtons) {
43914 var me = this,
43915 items = me.getItems(),
43916 pressedCls = me.getPressedCls(),
43917 events = [],
43918 item, button, ln, i, e;
43919
43920 //loop through existing items and remove the pressed cls from them
43921 ln = items.length;
43922 if (oldButtons && oldButtons.length) {
43923 for (i = 0; i < ln; i++) {
43924 item = items.items[i];
43925
43926 if (oldButtons.indexOf(item) != -1 && newButtons.indexOf(item) == -1) {
43927 item.removeCls([pressedCls, item.getPressedCls()]);
43928 events.push({
43929 item: item,
43930 toggle: false
43931 });
43932 }
43933 }
43934 }
43935
43936 //loop through the new pressed buttons and add the pressed cls to them
43937 ln = newButtons.length;
43938 for (i = 0; i < ln; i++) {
43939 button = newButtons[i];
43940 if (!oldButtons || oldButtons.indexOf(button) == -1) {
43941 button.addCls(pressedCls);
43942 events.push({
43943 item: button,
43944 toggle: true
43945 });
43946 }
43947 }
43948
43949 //loop through each of the events and fire them after a delay
43950 ln = events.length;
43951 if (ln && oldButtons !== undefined) {
43952 Ext.defer(function() {
43953 for (i = 0; i < ln; i++) {
43954 e = events[i];
43955 me.fireEvent('toggle', me, e.item, e.toggle);
43956 }
43957 }, 50);
43958 }
43959 },
43960
43961 /**
43962 * Returns `true` if a specified {@link Ext.Button} is pressed.
43963 * @param {Ext.Button} button The button to check if pressed.
43964 * @return {Boolean} pressed
43965 */
43966 isPressed: function(button) {
43967 var pressedButtons = this.getPressedButtons();
43968 return pressedButtons.indexOf(button) != -1;
43969 },
43970
43971 /**
43972 * @private
43973 */
43974 doSetDisabled: function(disabled) {
43975 var me = this;
43976
43977 me.items.each(function(item) {
43978 item.setDisabled(disabled);
43979 }, me);
43980
43981 me.callParent(arguments);
43982 }
43983 }, function() {
43984 });
43985
43986 /**
43987 * A mixin which allows a data component to be sorted
43988 * @ignore
43989 */
43990 Ext.define('Ext.Sortable', {
43991 mixins: {
43992 observable: Ext.mixin.Observable
43993 },
43994
43995
43996
43997 config: {
43998 /**
43999 * @cfg
44000 * @inheritdoc
44001 */
44002 baseCls: Ext.baseCSSPrefix + 'sortable',
44003
44004 /**
44005 * @cfg {Number} delay
44006 * How many milliseconds a user must hold the draggable before starting a
44007 * drag operation.
44008 * @private
44009 * @accessor
44010 */
44011 delay: 0
44012
44013 },
44014
44015 /**
44016 * @cfg {String} direction
44017 * Possible values: 'vertical', 'horizontal'.
44018 */
44019 direction: 'vertical',
44020
44021 /**
44022 * @cfg {String} cancelSelector
44023 * A simple CSS selector that represents elements within the draggable
44024 * that should NOT initiate a drag.
44025 */
44026 cancelSelector: null,
44027
44028 // not yet implemented
44029 //indicator: true,
44030 //proxy: true,
44031 //tolerance: null,
44032
44033 /**
44034 * @cfg {HTMLElement/Boolean} constrain
44035 * An Element to constrain the Sortable dragging to.
44036 * If `true` is specified, the dragging will be constrained to the element
44037 * of the sortable.
44038 */
44039 constrain: window,
44040 /**
44041 * @cfg {String} group
44042 * Draggable and Droppable objects can participate in a group which are
44043 * capable of interacting.
44044 */
44045 group: 'base',
44046
44047 /**
44048 * @cfg {Boolean} revert
44049 * This should NOT be changed.
44050 * @private
44051 */
44052 revert: true,
44053
44054 /**
44055 * @cfg {String} itemSelector
44056 * A simple CSS selector that represents individual items within the Sortable.
44057 */
44058 itemSelector: null,
44059
44060 /**
44061 * @cfg {String} handleSelector
44062 * A simple CSS selector to indicate what is the handle to drag the Sortable.
44063 */
44064 handleSelector: null,
44065
44066 /**
44067 * @cfg {Boolean} disabled
44068 * Passing in `true` will disable this Sortable.
44069 */
44070 disabled: false,
44071
44072 // Properties
44073
44074 /**
44075 * Read-only property that indicates whether a Sortable is currently sorting.
44076 * @type Boolean
44077 * @private
44078 * @readonly
44079 */
44080 sorting: false,
44081
44082 /**
44083 * Read-only value representing whether the Draggable can be moved vertically.
44084 * This is automatically calculated by Draggable by the direction configuration.
44085 * @type Boolean
44086 * @private
44087 * @readonly
44088 */
44089 vertical: false,
44090
44091 /**
44092 * Creates new Sortable.
44093 * @param {Mixed} el
44094 * @param {Object} config
44095 */
44096 constructor : function(el, config) {
44097 config = config || {};
44098 Ext.apply(this, config);
44099
44100 this.addEvents(
44101 /**
44102 * @event sortstart
44103 * @param {Ext.Sortable} this
44104 * @param {Ext.event.Event} e
44105 */
44106 'sortstart',
44107 /**
44108 * @event sortend
44109 * @param {Ext.Sortable} this
44110 * @param {Ext.event.Event} e
44111 */
44112 'sortend',
44113 /**
44114 * @event sortchange
44115 * @param {Ext.Sortable} this
44116 * @param {Ext.Element} el The Element being dragged.
44117 * @param {Number} index The index of the element after the sort change.
44118 */
44119 'sortchange'
44120
44121 // not yet implemented.
44122 // 'sortupdate',
44123 // 'sortreceive',
44124 // 'sortremove',
44125 // 'sortenter',
44126 // 'sortleave',
44127 // 'sortactivate',
44128 // 'sortdeactivate'
44129 );
44130
44131 this.el = Ext.get(el);
44132 this.callParent();
44133
44134 this.mixins.observable.constructor.call(this);
44135
44136 if (this.direction == 'horizontal') {
44137 this.horizontal = true;
44138 }
44139 else if (this.direction == 'vertical') {
44140 this.vertical = true;
44141 }
44142 else {
44143 this.horizontal = this.vertical = true;
44144 }
44145
44146 this.el.addCls(this.baseCls);
44147 this.startEventName = (this.getDelay() > 0) ? 'taphold' : 'tapstart';
44148 if (!this.disabled) {
44149 this.enable();
44150 }
44151 },
44152
44153 // @private
44154 onStart : function(e, t) {
44155 if (this.cancelSelector && e.getTarget(this.cancelSelector)) {
44156 return;
44157 }
44158 if (this.handleSelector && !e.getTarget(this.handleSelector)) {
44159 return;
44160 }
44161
44162 if (!this.sorting) {
44163 this.onSortStart(e, t);
44164 }
44165 },
44166
44167 // @private
44168 onSortStart : function(e, t) {
44169 this.sorting = true;
44170 var draggable = Ext.create('Ext.util.Draggable', t, {
44171 threshold: 0,
44172 revert: this.revert,
44173 direction: this.direction,
44174 constrain: this.constrain === true ? this.el : this.constrain,
44175 animationDuration: 100
44176 });
44177 draggable.on({
44178 drag: this.onDrag,
44179 dragend: this.onDragEnd,
44180 scope: this
44181 });
44182
44183 this.dragEl = t;
44184 this.calculateBoxes();
44185
44186 if (!draggable.dragging) {
44187 draggable.onStart(e);
44188 }
44189
44190 this.fireEvent('sortstart', this, e);
44191 },
44192
44193 // @private
44194 calculateBoxes : function() {
44195 this.items = [];
44196 var els = this.el.select(this.itemSelector, false),
44197 ln = els.length, i, item, el, box;
44198
44199 for (i = 0; i < ln; i++) {
44200 el = els[i];
44201 if (el != this.dragEl) {
44202 item = Ext.fly(el).getPageBox(true);
44203 item.el = el;
44204 this.items.push(item);
44205 }
44206 }
44207 },
44208
44209 // @private
44210 onDrag : function(draggable, e) {
44211 var items = this.items,
44212 ln = items.length,
44213 region = draggable.region,
44214 sortChange = false,
44215 i, intersect, overlap, item;
44216
44217 for (i = 0; i < ln; i++) {
44218 item = items[i];
44219 intersect = region.intersect(item);
44220 if (intersect) {
44221 if (this.vertical && Math.abs(intersect.top - intersect.bottom) > (region.bottom - region.top) / 2) {
44222 if (region.bottom > item.top && item.top > region.top) {
44223 draggable.el.insertAfter(item.el);
44224 }
44225 else {
44226 draggable.el.insertBefore(item.el);
44227 }
44228 sortChange = true;
44229 }
44230 else if (this.horizontal && Math.abs(intersect.left - intersect.right) > (region.right - region.left) / 2) {
44231 if (region.right > item.left && item.left > region.left) {
44232 draggable.el.insertAfter(item.el);
44233 }
44234 else {
44235 draggable.el.insertBefore(item.el);
44236 }
44237 sortChange = true;
44238 }
44239
44240 if (sortChange) {
44241 // We reset the draggable (initializes all the new start values)
44242 draggable.reset();
44243
44244 // Move the draggable to its current location (since the transform is now
44245 // different)
44246 draggable.moveTo(region.left, region.top);
44247
44248 // Finally lets recalculate all the items boxes
44249 this.calculateBoxes();
44250 this.fireEvent('sortchange', this, draggable.el, this.el.select(this.itemSelector, false).indexOf(draggable.el.dom));
44251 return;
44252 }
44253 }
44254 }
44255 },
44256
44257 // @private
44258 onDragEnd : function(draggable, e) {
44259 draggable.destroy();
44260 this.sorting = false;
44261 this.fireEvent('sortend', this, draggable, e);
44262 },
44263
44264 /**
44265 * Enables sorting for this Sortable.
44266 * This method is invoked immediately after construction of a Sortable unless
44267 * the disabled configuration is set to `true`.
44268 */
44269 enable : function() {
44270 this.el.on(this.startEventName, this.onStart, this, {delegate: this.itemSelector, holdThreshold: this.getDelay()});
44271 this.disabled = false;
44272 },
44273
44274 /**
44275 * Disables sorting for this Sortable.
44276 */
44277 disable : function() {
44278 this.el.un(this.startEventName, this.onStart, this);
44279 this.disabled = true;
44280 },
44281
44282 /**
44283 * Method to determine whether this Sortable is currently disabled.
44284 * @return {Boolean} The disabled state of this Sortable.
44285 */
44286 isDisabled: function() {
44287 return this.disabled;
44288 },
44289
44290 /**
44291 * Method to determine whether this Sortable is currently sorting.
44292 * @return {Boolean} The sorting state of this Sortable.
44293 */
44294 isSorting : function() {
44295 return this.sorting;
44296 },
44297
44298 /**
44299 * Method to determine whether this Sortable is currently disabled.
44300 * @return {Boolean} The disabled state of this Sortable.
44301 */
44302 isVertical : function() {
44303 return this.vertical;
44304 },
44305
44306 /**
44307 * Method to determine whether this Sortable is currently sorting.
44308 * @return {Boolean} The sorting state of this Sortable.
44309 */
44310 isHorizontal : function() {
44311 return this.horizontal;
44312 }
44313 });
44314
44315 /**
44316 * {@link Ext.TitleBar}'s are most commonly used as a docked item within an {@link Ext.Container}.
44317 *
44318 * The main difference between a {@link Ext.TitleBar} and an {@link Ext.Toolbar} is that
44319 * the {@link #title} configuration is **always** centered horizontally in a {@link Ext.TitleBar} between
44320 * any items aligned left or right.
44321 *
44322 * You can also give items of a {@link Ext.TitleBar} an `align` configuration of `left` or `right`
44323 * which will dock them to the `left` or `right` of the bar.
44324 *
44325 * ## Examples
44326 *
44327 * @example preview
44328 * Ext.Viewport.add({
44329 * xtype: 'titlebar',
44330 * docked: 'top',
44331 * title: 'Navigation',
44332 * items: [
44333 * {
44334 * iconCls: 'add',
44335 * align: 'left'
44336 * },
44337 * {
44338 * iconCls: 'home',
44339 * align: 'right'
44340 * }
44341 * ]
44342 * });
44343 *
44344 * Ext.Viewport.setStyleHtmlContent(true);
44345 * Ext.Viewport.setHtml('This shows the title being centered and buttons using align <i>left</i> and <i>right</i>.');
44346 *
44347 * <br />
44348 *
44349 * @example preview
44350 * Ext.Viewport.add({
44351 * xtype: 'titlebar',
44352 * docked: 'top',
44353 * title: 'Navigation',
44354 * items: [
44355 * {
44356 * align: 'left',
44357 * text: 'This button has a super long title'
44358 * },
44359 * {
44360 * iconCls: 'home',
44361 * align: 'right'
44362 * }
44363 * ]
44364 * });
44365 *
44366 * Ext.Viewport.setStyleHtmlContent(true);
44367 * Ext.Viewport.setHtml('This shows how the title is automatically moved to the right when one of the aligned buttons is very wide.');
44368 *
44369 * <br />
44370 *
44371 * @example preview
44372 * Ext.Viewport.add({
44373 * xtype: 'titlebar',
44374 * docked: 'top',
44375 * title: 'A very long title',
44376 * items: [
44377 * {
44378 * align: 'left',
44379 * text: 'This button has a super long title'
44380 * },
44381 * {
44382 * align: 'right',
44383 * text: 'Another button'
44384 * }
44385 * ]
44386 * });
44387 *
44388 * Ext.Viewport.setStyleHtmlContent(true);
44389 * Ext.Viewport.setHtml('This shows how the title and buttons will automatically adjust their size when the width of the items are too wide..');
44390 *
44391 * The {@link #defaultType} of Toolbar's is {@link Ext.Button button}.
44392 */
44393 Ext.define('Ext.TitleBar', {
44394 extend: Ext.Container ,
44395 xtype: 'titlebar',
44396
44397
44398
44399
44400
44401
44402
44403 // @private
44404 isToolbar: true,
44405
44406 config: {
44407 /**
44408 * @cfg
44409 * @inheritdoc
44410 */
44411 baseCls: Ext.baseCSSPrefix + 'toolbar',
44412
44413 /**
44414 * @cfg
44415 * @inheritdoc
44416 */
44417 cls: Ext.baseCSSPrefix + 'navigation-bar',
44418
44419 /**
44420 * @cfg {String} ui
44421 * Style options for Toolbar. Either 'light' or 'dark'.
44422 * @accessor
44423 */
44424 ui: 'dark',
44425
44426 /**
44427 * @cfg {String} title
44428 * The title of the toolbar.
44429 * @accessor
44430 */
44431 title: null,
44432
44433 /**
44434 * @cfg {String} titleAlign
44435 * The alignment for the title of the toolbar.
44436 * @accessor
44437 */
44438 titleAlign: 'center',
44439
44440 /**
44441 * @cfg {String} defaultType
44442 * The default xtype to create.
44443 * @accessor
44444 */
44445 defaultType: 'button',
44446
44447 /**
44448 * @cfg {String} minHeight
44449 * The minimum height height of the Toolbar.
44450 * @accessor
44451 */
44452 minHeight: null,
44453
44454 /**
44455 * @cfg
44456 * @hide
44457 */
44458 layout: {
44459 type: 'hbox'
44460 },
44461
44462 /**
44463 * @cfg {Array/Object} items The child items to add to this TitleBar. The {@link #defaultType} of
44464 * a TitleBar is {@link Ext.Button}, so you do not need to specify an `xtype` if you are adding
44465 * buttons.
44466 *
44467 * You can also give items a `align` configuration which will align the item to the `left` or `right` of
44468 * the TitleBar.
44469 * @accessor
44470 */
44471 items: [],
44472
44473 /**
44474 * @cfg {String} maxButtonWidth The maximum width of the button by percentage
44475 * @accessor
44476 */
44477 maxButtonWidth: '40%'
44478 },
44479
44480 platformConfig: [{
44481 theme: ['Blackberry', 'Blackberry103', 'Tizen'],
44482 titleAlign: 'left'
44483 }, {
44484 theme: ['Cupertino'],
44485 maxButtonWidth: '80%'
44486 }],
44487
44488 hasCSSMinHeight: true,
44489
44490 beforeInitialize: function() {
44491 this.applyItems = this.applyInitialItems;
44492 },
44493
44494 initialize: function() {
44495 delete this.applyItems;
44496
44497 this.add(this.initialItems);
44498 delete this.initialItems;
44499
44500 this.on({
44501 painted: 'refreshTitlePosition',
44502 single: true
44503 });
44504 },
44505
44506 applyInitialItems: function(items) {
44507 var me = this,
44508 titleAlign = me.getTitleAlign(),
44509 defaults = me.getDefaults() || {};
44510
44511 me.initialItems = items;
44512
44513 me.leftBox = me.add({
44514 xtype: 'container',
44515 style: 'position: relative',
44516 layout: {
44517 type: 'hbox',
44518 align: 'center'
44519 },
44520 listeners: {
44521 resize: 'refreshTitlePosition',
44522 scope: me
44523 }
44524 });
44525
44526 me.spacer = me.add({
44527 xtype: 'component',
44528 style: 'position: relative',
44529 flex: 1,
44530 listeners: {
44531 resize: 'refreshTitlePosition',
44532 scope: me
44533 }
44534 });
44535
44536 me.rightBox = me.add({
44537 xtype: 'container',
44538 style: 'position: relative',
44539 layout: {
44540 type: 'hbox',
44541 align: 'center'
44542 },
44543 listeners: {
44544 resize: 'refreshTitlePosition',
44545 scope: me
44546 }
44547 });
44548
44549
44550 switch(titleAlign) {
44551 case 'left':
44552 me.titleComponent = me.leftBox.add({
44553 xtype: 'title',
44554 cls: Ext.baseCSSPrefix + 'title-align-left',
44555 hidden: defaults.hidden
44556 });
44557 me.refreshTitlePosition = Ext.emptyFn;
44558 break;
44559 case 'right':
44560 me.titleComponent = me.rightBox.add({
44561 xtype: 'title',
44562 cls: Ext.baseCSSPrefix + 'title-align-right',
44563 hidden: defaults.hidden
44564 });
44565 me.refreshTitlePosition = Ext.emptyFn;
44566 break;
44567 default:
44568 me.titleComponent = me.add({
44569 xtype: 'title',
44570 hidden: defaults.hidden,
44571 centered: true
44572 });
44573 break;
44574 }
44575
44576 me.doAdd = me.doBoxAdd;
44577 me.remove = me.doBoxRemove;
44578 me.doInsert = me.doBoxInsert;
44579 },
44580
44581 doBoxAdd: function(item) {
44582 if (item.config.align == 'right') {
44583 this.rightBox.add(item);
44584 }
44585 else {
44586 this.leftBox.add(item);
44587 }
44588 },
44589
44590 doBoxRemove: function(item, destroy) {
44591 if (item.config.align == 'right') {
44592 this.rightBox.remove(item, destroy);
44593 }
44594 else {
44595 this.leftBox.remove(item, destroy);
44596 }
44597 },
44598
44599 doBoxInsert: function(index, item) {
44600 if (item.config.align == 'right') {
44601 this.rightBox.insert(index, item);
44602 }
44603 else {
44604 this.leftBox.insert(index, item);
44605 }
44606 },
44607
44608 calculateMaxButtonWidth: function() {
44609 var maxButtonWidth = this.getMaxButtonWidth();
44610
44611 //check if it is a percentage
44612 if (Ext.isString(maxButtonWidth)) {
44613 maxButtonWidth = parseInt(maxButtonWidth.replace('%', ''), 10);
44614 }
44615 maxButtonWidth = Math.round((this.element.getWidth() / 100) * maxButtonWidth);
44616
44617 return maxButtonWidth;
44618 },
44619
44620 refreshTitlePosition: function() {
44621 if (this.isDestroyed) {
44622 return;
44623 }
44624
44625 var titleElement = this.titleComponent.renderElement;
44626
44627 titleElement.setWidth(null);
44628 titleElement.setLeft(null);
44629
44630 //set the min/max width of the left button
44631 var leftBox = this.leftBox,
44632 leftButton = leftBox.down('button'),
44633 singleButton = leftBox.getItems().getCount() == 1,
44634 leftBoxWidth, maxButtonWidth;
44635
44636 if (leftButton && singleButton) {
44637 if (leftButton.getWidth() == null) {
44638 leftButton.renderElement.setWidth('auto');
44639 }
44640
44641 leftBoxWidth = leftBox.renderElement.getWidth();
44642 maxButtonWidth = this.calculateMaxButtonWidth();
44643
44644 if (leftBoxWidth > maxButtonWidth) {
44645 leftButton.renderElement.setWidth(maxButtonWidth);
44646 }
44647 }
44648
44649 var spacerBox = this.spacer.renderElement.getPageBox();
44650
44651 if (Ext.browser.is.IE) {
44652 titleElement.setWidth(spacerBox.width);
44653 }
44654
44655 var titleBox = titleElement.getPageBox(),
44656 widthDiff = titleBox.width - spacerBox.width,
44657 titleLeft = titleBox.left,
44658 titleRight = titleBox.right,
44659 halfWidthDiff, leftDiff, rightDiff;
44660
44661
44662 if (widthDiff > 0) {
44663 halfWidthDiff = widthDiff / 2;
44664 titleLeft += halfWidthDiff;
44665 titleRight -= halfWidthDiff;
44666 titleElement.setWidth(spacerBox.width);
44667 }
44668
44669 leftDiff = spacerBox.left - titleLeft;
44670 rightDiff = titleRight - spacerBox.right;
44671
44672 if (leftDiff > 0) {
44673 titleElement.setLeft(leftDiff);
44674 }
44675 else if (rightDiff > 0) {
44676 titleElement.setLeft(-rightDiff);
44677 }
44678
44679 titleElement.repaint();
44680 },
44681
44682 // @private
44683 updateTitle: function(newTitle) {
44684 this.titleComponent.setTitle(newTitle);
44685
44686 if (this.isPainted()) {
44687 this.refreshTitlePosition();
44688 }
44689 }
44690 });
44691
44692 /**
44693 * A 'Toast' is a simple modal message that is displayed on the screen and then automatically closed by a timeout or by a user tapping
44694 * outside of the toast itself. Think about it like a text only alert box that will self destruct. **A Toast should not be instantiated manually**
44695 * but creating by calling 'Ext.toast(message, timeout)'. This will create one reusable toast container and content will be swapped out as
44696 * toast messages are queued or displayed.
44697 *
44698 * # Simple Toast
44699 *
44700 * @example miniphone
44701 * Ext.toast('Hello Sencha!'); // Toast will close in 1000 milliseconds (default)
44702 *
44703 * # Toast with Timeout
44704 *
44705 * @example miniphone
44706 * Ext.toast('Hello Sencha!', 5000); // Toast will close in 5000 milliseconds
44707 *
44708 * # Toast with config
44709 *
44710 * @example miniphone
44711 * Ext.toast({message: 'Hello Sencha!', timeout: 2000}); // Toast will close in 2000 milliseconds
44712 *
44713 * # Multiple Toasts queued
44714 *
44715 * @example miniphone
44716 * Ext.toast('Hello Sencha!');
44717 * Ext.toast('Hello Sencha Again!');
44718 * Ext.toast('Hello Sencha One More Time!');
44719 */
44720 Ext.define('Ext.Toast', {
44721 extend: Ext.Sheet ,
44722
44723
44724
44725
44726 config: {
44727 /**
44728 * @cfg
44729 * @inheritdoc
44730 */
44731 ui: 'dark',
44732
44733 /**
44734 * @cfg
44735 * @inheritdoc
44736 */
44737 baseCls: Ext.baseCSSPrefix + 'toast',
44738
44739 /**
44740 * @cfg
44741 * @inheritdoc
44742 */
44743 showAnimation: {
44744 type: 'popIn',
44745 duration: 250,
44746 easing: 'ease-out'
44747 },
44748
44749 /**
44750 * @cfg
44751 * @inheritdoc
44752 */
44753 hideAnimation: {
44754 type: 'popOut',
44755 duration: 250,
44756 easing: 'ease-out'
44757 },
44758
44759 /**
44760 * Override the default `zIndex` so it is normally always above floating components.
44761 */
44762 zIndex: 999,
44763
44764 /**
44765 * @cfg {String} message
44766 * The message to be displayed in the {@link Ext.Toast}.
44767 * @accessor
44768 */
44769 message: null,
44770
44771 /**
44772 * @cfg {Number} timeout
44773 * The amount of time in milliseconds to wait before destroying the toast automatically
44774 */
44775 timeout: 1000,
44776
44777 /**
44778 * @cfg{Boolean/Object} animation
44779 * The animation that should be used between toast messages when they are queued up
44780 */
44781 messageAnimation: true,
44782
44783 /**
44784 * @cfg
44785 * @inheritdoc
44786 */
44787 hideOnMaskTap: true,
44788
44789 /**
44790 * @private
44791 */
44792 modal: true,
44793
44794 /**
44795 * @cfg
44796 * @inheritdoc
44797 */
44798 layout: {
44799 type: 'vbox',
44800 pack: 'center'
44801 }
44802 },
44803
44804 /**
44805 * @private
44806 */
44807 applyMessage: function(config) {
44808 config = {
44809 html: config,
44810 cls: this.getBaseCls() + '-text'
44811 };
44812
44813 return Ext.factory(config, Ext.Component, this._message);
44814 },
44815
44816 /**
44817 * @private
44818 */
44819 updateMessage: function(newMessage) {
44820 if (newMessage) {
44821 this.add(newMessage);
44822 }
44823 },
44824
44825 /**
44826 * @private
44827 */
44828 applyTimeout: function(timeout) {
44829 if (this._timeoutID) {
44830 clearTimeout(this._timeoutID);
44831 if (!Ext.isEmpty(timeout)) {
44832 this._timeoutID = setTimeout(Ext.bind(this.onTimeout, this), timeout);
44833 }
44834 }
44835 return timeout;
44836 },
44837
44838 /**
44839 * @internal
44840 */
44841 next: Ext.emptyFn,
44842
44843 /**
44844 * @private
44845 */
44846 show: function(config) {
44847 var me = this,
44848 timeout = config.timeout,
44849 msgAnimation = me.getMessageAnimation(),
44850 message = me.getMessage();
44851
44852 if (me.isRendered() && me.isHidden() === false) {
44853 config.timeout = null;
44854 message.onAfter({
44855 hiddenchange: function() {
44856 me.setMessage(config.message);
44857 message = me.getMessage();
44858 message.onAfter({
44859 hiddenchange: function() {
44860
44861 // Forces applyTimeout to create a timer
44862 this._timeoutID = true;
44863 me.setTimeout(timeout);
44864 },
44865 scope: me,
44866 single: true
44867 });
44868 message.show(msgAnimation);
44869 },
44870 scope: me,
44871 single: true
44872 });
44873
44874 message.hide(msgAnimation);
44875 } else {
44876 Ext.util.InputBlocker.blockInputs();
44877 me.setConfig(config);
44878
44879 //if it has not been added to a container, add it to the Viewport.
44880 if (!me.getParent() && Ext.Viewport) {
44881 Ext.Viewport.add(me);
44882 }
44883
44884 if (!Ext.isEmpty(timeout)) {
44885 me._timeoutID = setTimeout(Ext.bind(me.onTimeout, me), timeout);
44886 }
44887
44888 me.callParent(arguments);
44889 }
44890 },
44891
44892 /**
44893 * @private
44894 */
44895 hide: function(animation) {
44896 clearTimeout(this._timeoutID);
44897 if (!this.next()) {
44898 this.callParent(arguments);
44899 }
44900 },
44901
44902 /**
44903 * @private
44904 */
44905 onTimeout: function() {
44906 this.hide();
44907 }
44908 }, function(Toast) {
44909 var _queue = [], _isToasting = false;
44910
44911 function next() {
44912 var config = _queue.shift();
44913
44914 if (config) {
44915 _isToasting = true;
44916 this.show(config);
44917 } else {
44918 _isToasting = false;
44919 }
44920
44921 return _isToasting;
44922 }
44923
44924 function getInstance() {
44925 if (!Ext.Toast._instance) {
44926 Ext.Toast._instance = Ext.create('Ext.Toast');
44927 Ext.Toast._instance.next = next;
44928 }
44929 return Ext.Toast._instance;
44930 }
44931
44932 Ext.toast = function(message, timeout) {
44933 var toast = getInstance(),
44934 config = message;
44935
44936 if (Ext.isString(message)) {
44937 config = {
44938 message: message,
44939 timeout: timeout
44940 };
44941 }
44942
44943 if (config.timeout === undefined) {
44944 config.timeout = Ext.Toast.prototype.config.timeout;
44945 }
44946
44947 _queue.push(config);
44948 if (!_isToasting) {
44949 toast.next();
44950 }
44951
44952 return toast;
44953 }
44954 });
44955
44956
44957 /**
44958 * Provides a simple Container for HTML5 Video.
44959 *
44960 * ## Notes
44961 *
44962 * - There are quite a few issues with the `<video>` tag on Android devices. On Android 2+, the video will
44963 * appear and play on first attempt, but any attempt afterwards will not work.
44964 *
44965 * ## Useful Properties
44966 *
44967 * - {@link #url}
44968 * - {@link #autoPause}
44969 * - {@link #autoResume}
44970 *
44971 * ## Useful Methods
44972 *
44973 * - {@link #method-pause}
44974 * - {@link #method-play}
44975 * - {@link #toggle}
44976 *
44977 * ## Example
44978 *
44979 * var panel = Ext.create('Ext.Panel', {
44980 * fullscreen: true,
44981 * items: [
44982 * {
44983 * xtype : 'video',
44984 * x : 600,
44985 * y : 300,
44986 * width : 175,
44987 * height : 98,
44988 * url : "porsche911.mov",
44989 * posterUrl: 'porsche.png'
44990 * }
44991 * ]
44992 * });
44993 */
44994 Ext.define('Ext.Video', {
44995 extend: Ext.Media ,
44996 xtype: 'video',
44997
44998 config: {
44999 /**
45000 * @cfg {String/Array} url
45001 * Location of the video to play. This should be in H.264 format and in a .mov file format.
45002 * @accessor
45003 */
45004
45005 /**
45006 * @cfg {String} posterUrl
45007 * Location of a poster image to be shown before showing the video.
45008 * @accessor
45009 */
45010 posterUrl: null,
45011
45012 /**
45013 * @cfg
45014 * @inheritdoc
45015 */
45016 baseCls: Ext.baseCSSPrefix + 'video',
45017
45018 /**
45019 * @cfg {Boolean} controls
45020 * Determines if native controls should be shown for this video player.
45021 */
45022 controls: true
45023 },
45024
45025 template: [{
45026 /**
45027 * @property {Ext.dom.Element} ghost
45028 * @private
45029 */
45030 reference: 'ghost',
45031 classList: [Ext.baseCSSPrefix + 'video-ghost']
45032 }, {
45033 tag: 'video',
45034 reference: 'media',
45035 classList: [Ext.baseCSSPrefix + 'media']
45036 }],
45037
45038 initialize: function() {
45039 var me = this;
45040
45041 me.callParent();
45042
45043 me.media.hide();
45044
45045 me.onBefore({
45046 erased: 'onErased',
45047 scope: me
45048 });
45049
45050 me.ghost.on({
45051 tap: 'onGhostTap',
45052 scope: me
45053 });
45054
45055 me.media.on({
45056 pause: 'onPause',
45057 scope: me
45058 });
45059
45060 if (Ext.os.is.Android4 || Ext.os.is.iPad) {
45061 this.isInlineVideo = true;
45062 }
45063 },
45064
45065 applyUrl: function(url) {
45066 return [].concat(url);
45067 },
45068
45069 updateUrl: function(newUrl) {
45070 var me = this,
45071 media = me.media,
45072 newLn = newUrl.length,
45073 existingSources = media.query('source'),
45074 oldLn = existingSources.length,
45075 i;
45076
45077
45078 for (i = 0; i < oldLn; i++) {
45079 Ext.fly(existingSources[i]).destroy();
45080 }
45081
45082 for (i = 0; i < newLn; i++) {
45083 media.appendChild(Ext.Element.create({
45084 tag: 'source',
45085 src: newUrl[i]
45086 }));
45087 }
45088
45089 if (me.isPlaying()) {
45090 me.play();
45091 }
45092 },
45093
45094 updateControls: function(value) {
45095 this.media.set({controls:value ? true : undefined});
45096 },
45097
45098 onErased: function() {
45099 this.pause();
45100 this.media.setTop(-2000);
45101 this.ghost.show();
45102 },
45103
45104 /**
45105 * @private
45106 * Called when the {@link #ghost} element is tapped.
45107 */
45108 onGhostTap: function() {
45109 var me = this,
45110 media = this.media,
45111 ghost = this.ghost;
45112
45113 media.show();
45114 if (Ext.browser.is.AndroidStock2) {
45115 setTimeout(function() {
45116 me.play();
45117 setTimeout(function() {
45118 media.hide();
45119 }, 10);
45120 }, 10);
45121 } else {
45122 // Browsers which support native video tag display only, move the media down so
45123 // we can control the Viewport
45124 ghost.hide();
45125 me.play();
45126 }
45127 },
45128
45129 /**
45130 * @private
45131 * native video tag display only, move the media down so we can control the Viewport
45132 */
45133 onPause: function() {
45134 this.callParent(arguments);
45135 if (!this.isInlineVideo) {
45136 this.media.setTop(-2000);
45137 this.ghost.show();
45138 }
45139 },
45140
45141 /**
45142 * @private
45143 * native video tag display only, move the media down so we can control the Viewport
45144 */
45145 onPlay: function() {
45146 this.callParent(arguments);
45147 this.media.setTop(0);
45148 },
45149
45150 /**
45151 * Updates the URL to the poster, even if it is rendered.
45152 * @param {Object} newUrl
45153 */
45154 updatePosterUrl: function(newUrl) {
45155 var ghost = this.ghost;
45156 if (ghost) {
45157 ghost.setStyle('background-image', 'url(' + newUrl + ')');
45158 }
45159 }
45160 });
45161
45162 /**
45163 * @author Ed Spencer
45164 * @private
45165 *
45166 * Represents a single action as {@link Ext.app.Application#dispatch dispatched} by an Application. This is typically
45167 * generated as a result of a url change being matched by a Route, triggering Application's dispatch function.
45168 *
45169 * This is a private class and its functionality and existence may change in the future. Use at your own risk.
45170 *
45171 */
45172 Ext.define('Ext.app.Action', {
45173 config: {
45174 /**
45175 * @cfg {Object} scope The scope in which the {@link #action} should be called.
45176 */
45177 scope: null,
45178
45179 /**
45180 * @cfg {Ext.app.Application} application The Application that this Action is bound to.
45181 */
45182 application: null,
45183
45184 /**
45185 * @cfg {Ext.app.Controller} controller The {@link Ext.app.Controller controller} whose {@link #action} should
45186 * be called.
45187 */
45188 controller: null,
45189
45190 /**
45191 * @cfg {String} action The name of the action on the {@link #controller} that should be called.
45192 */
45193 action: null,
45194
45195 /**
45196 * @cfg {Array} args The set of arguments that will be passed to the controller's {@link #action}.
45197 */
45198 args: [],
45199
45200 /**
45201 * @cfg {String} url The url that was decoded into the controller/action/args in this Action.
45202 */
45203 url: undefined,
45204 data: {},
45205 title: null,
45206
45207 /**
45208 * @cfg {Array} beforeFilters The (optional) set of functions to call before the {@link #action} is called.
45209 * This is usually handled directly by the Controller or Application when an Ext.app.Action instance is
45210 * created, but is alterable before {@link #resume} is called.
45211 * @accessor
45212 */
45213 beforeFilters: [],
45214
45215 /**
45216 * @private
45217 * Keeps track of which before filter is currently being executed by {@link #resume}
45218 */
45219 currentFilterIndex: -1
45220 },
45221
45222 constructor: function(config) {
45223 this.initConfig(config);
45224
45225 this.getUrl();
45226 },
45227
45228 applyBeforeFilters: function(filters) {
45229 return filters || [];
45230 },
45231
45232 /**
45233 * Starts execution of this Action by calling each of the {@link #beforeFilters} in turn (if any are specified),
45234 * before calling the Controller {@link #action}. Same as calling {@link #resume}.
45235 */
45236 execute: function() {
45237 this.resume();
45238 },
45239
45240 /**
45241 * Resumes the execution of this Action (or starts it if it had not been started already). This iterates over all
45242 * of the configured {@link #beforeFilters} and calls them. Each before filter is called with this Action as the
45243 * sole argument, and is expected to call `action.resume()` in order to allow the next filter to be called, or if
45244 * this is the final filter, the original {@link Ext.app.Controller Controller} function.
45245 */
45246 resume: function() {
45247 var index = this.getCurrentFilterIndex() + 1,
45248 filters = this.getBeforeFilters(),
45249 controller = this.getController(),
45250 nextFilter = filters[index];
45251
45252 if (nextFilter) {
45253 this.setCurrentFilterIndex(index);
45254 nextFilter.call(controller, this);
45255 } else {
45256 controller[this.getAction()].apply(controller, this.getArgs());
45257 }
45258 },
45259
45260 /**
45261 * @private
45262 */
45263 applyUrl: function(url) {
45264 if (url === null || url === undefined) {
45265 url = this.urlEncode();
45266 }
45267
45268 return url;
45269 },
45270
45271 /**
45272 * @private
45273 * If the controller config is a string, swap it for a reference to the actual controller instance.
45274 * @param {String} controller The controller name.
45275 */
45276 applyController: function(controller) {
45277 var app = this.getApplication(),
45278 profile = app.getCurrentProfile();
45279
45280 if (Ext.isString(controller)) {
45281 controller = app.getController(controller, profile ? profile.getNamespace() : null);
45282 }
45283
45284 return controller;
45285 },
45286
45287 /**
45288 * @private
45289 */
45290 urlEncode: function() {
45291 var controller = this.getController(),
45292 splits;
45293
45294 if (controller instanceof Ext.app.Controller) {
45295 splits = controller.$className.split('.');
45296 controller = splits[splits.length - 1];
45297 }
45298
45299 return controller + "/" + this.getAction();
45300 }
45301 });
45302
45303 /**
45304 * @author Ed Spencer
45305 *
45306 * Controllers are responsible for responding to events that occur within your app. If your app contains a Logout
45307 * {@link Ext.Button button} that your user can tap on, a Controller would listen to the Button's tap event and take
45308 * the appropriate action. It allows the View classes to handle the display of data and the Model classes to handle the
45309 * loading and saving of data - the Controller is the glue that binds them together.
45310 *
45311 * ## Relation to Ext.app.Application
45312 *
45313 * Controllers exist within the context of an {@link Ext.app.Application Application}. An Application usually consists
45314 * of a number of Controllers, each of which handle a specific part of the app. For example, an Application that
45315 * handles the orders for an online shopping site might have controllers for Orders, Customers and Products.
45316 *
45317 * All of the Controllers that an Application uses are specified in the Application's
45318 * {@link Ext.app.Application#controllers} config. The Application automatically instantiates each Controller and keeps
45319 * references to each, so it is unusual to need to instantiate Controllers directly. By convention each Controller is
45320 * named after the thing (usually the Model) that it deals with primarily, usually in the plural - for example if your
45321 * app is called 'MyApp' and you have a Controller that manages Products, convention is to create a
45322 * MyApp.controller.Products class in the file app/controller/Products.js.
45323 *
45324 * ## Refs and Control
45325 *
45326 * The centerpiece of Controllers is the twin configurations {@link #refs} and {@link #cfg-control}. These are used to
45327 * easily gain references to Components inside your app and to take action on them based on events that they fire.
45328 * Let's look at {@link #refs} first:
45329 *
45330 * ### Refs
45331 *
45332 * Refs leverage the powerful {@link Ext.ComponentQuery ComponentQuery} syntax to easily locate Components on your
45333 * page. We can define as many refs as we like for each Controller, for example here we define a ref called 'nav' that
45334 * finds a Component on the page with the ID 'mainNav'. We then use that ref in the addLogoutButton beneath it:
45335 *
45336 * Ext.define('MyApp.controller.Main', {
45337 * extend: 'Ext.app.Controller',
45338 *
45339 * config: {
45340 * refs: {
45341 * nav: '#mainNav'
45342 * }
45343 * },
45344 *
45345 * addLogoutButton: function() {
45346 * this.getNav().add({
45347 * text: 'Logout'
45348 * });
45349 * }
45350 * });
45351 *
45352 * Usually, a ref is just a key/value pair - the key ('nav' in this case) is the name of the reference that will be
45353 * generated, the value ('#mainNav' in this case) is the {@link Ext.ComponentQuery ComponentQuery} selector that will
45354 * be used to find the Component.
45355 *
45356 * Underneath that, we have created a simple function called addLogoutButton which uses this ref via its generated
45357 * 'getNav' function. These getter functions are generated based on the refs you define and always follow the same
45358 * format - 'get' followed by the capitalized ref name. In this case we're treating the nav reference as though it's a
45359 * {@link Ext.Toolbar Toolbar}, and adding a Logout button to it when our function is called. This ref would recognize
45360 * a Toolbar like this:
45361 *
45362 * Ext.create('Ext.Toolbar', {
45363 * id: 'mainNav',
45364 *
45365 * items: [
45366 * {
45367 * text: 'Some Button'
45368 * }
45369 * ]
45370 * });
45371 *
45372 * Assuming this Toolbar has already been created by the time we run our 'addLogoutButton' function (we'll see how that
45373 * is invoked later), it will get the second button added to it.
45374 *
45375 * ### Advanced Refs
45376 *
45377 * Refs can also be passed a couple of additional options, beyond name and selector. These are autoCreate and xtype,
45378 * which are almost always used together:
45379 *
45380 * Ext.define('MyApp.controller.Main', {
45381 * extend: 'Ext.app.Controller',
45382 *
45383 * config: {
45384 * refs: {
45385 * nav: '#mainNav',
45386 *
45387 * infoPanel: {
45388 * selector: 'tabpanel panel[name=fish] infopanel',
45389 * xtype: 'infopanel',
45390 * autoCreate: true
45391 * }
45392 * }
45393 * }
45394 * });
45395 *
45396 * We've added a second ref to our Controller. Again the name is the key, 'infoPanel' in this case, but this time we've
45397 * passed an object as the value instead. This time we've used a slightly more complex selector query - in this example
45398 * imagine that your app contains a {@link Ext.tab.Panel tab panel} and that one of the items in the tab panel has been
45399 * given the name 'fish'. Our selector matches any Component with the xtype 'infopanel' inside that tab panel item.
45400 *
45401 * The difference here is that if that infopanel does not exist already inside the 'fish' panel, it will be
45402 * automatically created when you call this.getInfoPanel inside your Controller. The Controller is able to do this
45403 * because we provided the xtype to instantiate with in the event that the selector did not return anything.
45404 *
45405 * ### Control
45406 *
45407 * The sister config to {@link #refs} is {@link #cfg-control}. {@link #cfg-control Control} is the means by which your listen
45408 * to events fired by Components and have your Controller react in some way. Control accepts both ComponentQuery
45409 * selectors and refs as its keys, and listener objects as values - for example:
45410 *
45411 * Ext.define('MyApp.controller.Main', {
45412 * extend: 'Ext.app.Controller',
45413 *
45414 * config: {
45415 * control: {
45416 * loginButton: {
45417 * tap: 'doLogin'
45418 * },
45419 * 'button[action=logout]': {
45420 * tap: 'doLogout'
45421 * }
45422 * },
45423 *
45424 * refs: {
45425 * loginButton: 'button[action=login]'
45426 * }
45427 * },
45428 *
45429 * doLogin: function() {
45430 * //called whenever the Login button is tapped
45431 * },
45432 *
45433 * doLogout: function() {
45434 * //called whenever any Button with action=logout is tapped
45435 * }
45436 * });
45437 *
45438 * Here we have set up two control declarations - one for our loginButton ref and the other for any Button on the page
45439 * that has been given the action 'logout'. For each declaration we passed in a single event handler - in each case
45440 * listening for the 'tap' event, specifying the action that should be called when that Button fires the tap event.
45441 * Note that we specified the 'doLogin' and 'doLogout' methods as strings inside the control block - this is important.
45442 *
45443 * You can listen to as many events as you like in each control declaration, and mix and match ComponentQuery selectors
45444 * and refs as the keys.
45445 *
45446 * ## Routes
45447 *
45448 * As of Sencha Touch 2, Controllers can now directly specify which routes they are interested in. This enables us to
45449 * provide history support within our app, as well as the ability to deeply link to any part of the application that we
45450 * provide a route for.
45451 *
45452 * For example, let's say we have a Controller responsible for logging in and viewing user profiles, and want to make
45453 * those screens accessible via urls. We could achieve that like this:
45454 *
45455 * Ext.define('MyApp.controller.Users', {
45456 * extend: 'Ext.app.Controller',
45457 *
45458 * config: {
45459 * routes: {
45460 * 'login': 'showLogin',
45461 * 'user/:id': 'showUserById'
45462 * },
45463 *
45464 * refs: {
45465 * main: '#mainTabPanel'
45466 * }
45467 * },
45468 *
45469 * //uses our 'main' ref above to add a loginpanel to our main TabPanel (note that
45470 * //'loginpanel' is a custom xtype created for this application)
45471 * showLogin: function() {
45472 * this.getMain().add({
45473 * xtype: 'loginpanel'
45474 * });
45475 * },
45476 *
45477 * //Loads the User then adds a 'userprofile' view to the main TabPanel
45478 * showUserById: function(id) {
45479 * MyApp.model.User.load(id, {
45480 * scope: this,
45481 * success: function(user) {
45482 * this.getMain().add({
45483 * xtype: 'userprofile',
45484 * user: user
45485 * });
45486 * }
45487 * });
45488 * }
45489 * });
45490 *
45491 * The routes we specified above simply map the contents of the browser address bar to a Controller function to call
45492 * when that route is matched. The routes can be simple text like the login route, which matches against
45493 * http://myapp.com/#login, or contain wildcards like the 'user/:id' route, which matches urls like
45494 * http://myapp.com/#user/123. Whenever the address changes the Controller automatically calls the function specified.
45495 *
45496 * Note that in the showUserById function we had to first load the User instance. Whenever you use a route, the
45497 * function that is called by that route is completely responsible for loading its data and restoring state. This is
45498 * because your user could either send that url to another person or simply refresh the page, which we wipe clear any
45499 * cached data you had already loaded. There is a more thorough discussion of restoring state with routes in the
45500 * [application architecture guides](../../../core_concepts/about_applications.html).
45501 *
45502 * ## Advanced Usage
45503 *
45504 * See [the Controllers guide](../../../core_concepts/controllers.html) for advanced Controller
45505 * usage including before filters and customizing for different devices.
45506 */
45507 Ext.define('Ext.app.Controller', {
45508 mixins: {
45509 observable: Ext.mixin.Observable
45510 },
45511
45512 config: {
45513 /**
45514 * @cfg {Object} refs A collection of named {@link Ext.ComponentQuery ComponentQuery} selectors that makes it
45515 * easy to get references to key Components on your page. Example usage:
45516 *
45517 * refs: {
45518 * main: '#mainTabPanel',
45519 * loginButton: '#loginWindow button[action=login]',
45520 *
45521 * infoPanel: {
45522 * selector: 'infopanel',
45523 * xtype: 'infopanel',
45524 * autoCreate: true
45525 * }
45526 * }
45527 *
45528 * The first two are simple ComponentQuery selectors, the third (infoPanel) also passes in the autoCreate and
45529 * xtype options, which will first run the ComponentQuery to see if a Component matching that selector exists
45530 * on the page. If not, it will automatically create one using the xtype provided:
45531 *
45532 * someControllerFunction: function() {
45533 * //if the info panel didn't exist before, calling its getter will instantiate
45534 * //it automatically and return the new instance
45535 * this.getInfoPanel().show();
45536 * }
45537 *
45538 * @accessor
45539 */
45540 refs: {},
45541
45542 /**
45543 * @cfg {Object} routes Provides a mapping of urls to Controller actions. Whenever the specified url is matched
45544 * in the address bar, the specified Controller action is called. Example usage:
45545 *
45546 * routes: {
45547 * 'login': 'showLogin',
45548 * 'users/:id': 'showUserById'
45549 * }
45550 *
45551 * The first route will match against http://myapp.com/#login and call the Controller's showLogin function. The
45552 * second route contains a wildcard (':id') and will match all urls like http://myapp.com/#users/123, calling
45553 * the showUserById function with the matched ID as the first argument.
45554 *
45555 * @accessor
45556 */
45557 routes: {},
45558
45559 /**
45560 * @cfg {Object} control Provides a mapping of Controller functions that should be called whenever certain
45561 * Component events are fired. The Components can be specified using {@link Ext.ComponentQuery ComponentQuery}
45562 * selectors or {@link #refs}. Example usage:
45563 *
45564 * control: {
45565 * 'button[action=logout]': {
45566 * tap: 'doLogout'
45567 * },
45568 * main: {
45569 * activeitemchange: 'doUpdate'
45570 * }
45571 * }
45572 *
45573 * The first item uses a ComponentQuery selector to run the Controller's doLogout function whenever any Button
45574 * with action=logout is tapped on. The second calls the Controller's doUpdate function whenever the
45575 * activeitemchange event is fired by the Component referenced by our 'main' ref. In this case main is a tab
45576 * panel (see {@link #refs} for how to set that reference up).
45577 *
45578 * @accessor
45579 */
45580 control: {},
45581
45582 /**
45583 * @cfg {Object} before Provides a mapping of Controller functions to filter functions that are run before them
45584 * when dispatched to from a route. These are usually used to run pre-processing functions like authentication
45585 * before a certain function is executed. They are only called when dispatching from a route. Example usage:
45586 *
45587 * Ext.define('MyApp.controller.Products', {
45588 * config: {
45589 * before: {
45590 * editProduct: 'authenticate'
45591 * },
45592 *
45593 * routes: {
45594 * 'product/edit/:id': 'editProduct'
45595 * }
45596 * },
45597 *
45598 * //this is not directly because our before filter is called first
45599 * editProduct: function() {
45600 * //... performs the product editing logic
45601 * },
45602 *
45603 * //this is run before editProduct
45604 * authenticate: function(action) {
45605 * MyApp.authenticate({
45606 * success: function() {
45607 * action.resume();
45608 * },
45609 * failure: function() {
45610 * Ext.Msg.alert('Not Logged In', "You can't do that, you're not logged in");
45611 * }
45612 * });
45613 * }
45614 * });
45615 *
45616 * @accessor
45617 */
45618 before: {},
45619
45620 /**
45621 * @cfg {Ext.app.Application} application The Application instance this Controller is attached to. This is
45622 * automatically provided when using the MVC architecture so should rarely need to be set directly.
45623 * @accessor
45624 */
45625 application: {},
45626
45627 /**
45628 * @cfg {String[]} stores The set of stores to load for this Application. Each store is expected to
45629 * exist inside the *app/store* directory and define a class following the convention
45630 * AppName.store.StoreName. For example, in the code below, the *AppName.store.Users* class will be loaded.
45631 * Note that we are able to specify either the full class name (as with *AppName.store.Groups*) or just the
45632 * final part of the class name and leave Application to automatically prepend *AppName.store.'* to each:
45633 *
45634 * stores: [
45635 * 'Users',
45636 * 'AppName.store.Groups',
45637 * 'SomeCustomNamespace.store.Orders'
45638 * ]
45639 * @accessor
45640 */
45641 stores: [],
45642
45643 /**
45644 * @cfg {String[]} models The set of models to load for this Application. Each model is expected to exist inside the
45645 * *app/model* directory and define a class following the convention AppName.model.ModelName. For example, in the
45646 * code below, the classes *AppName.model.User*, *AppName.model.Group* and *AppName.model.Product* will be loaded.
45647 * Note that we are able to specify either the full class name (as with *AppName.model.Product*) or just the
45648 * final part of the class name and leave Application to automatically prepend *AppName.model.* to each:
45649 *
45650 * models: [
45651 * 'User',
45652 * 'Group',
45653 * 'AppName.model.Product',
45654 * 'SomeCustomNamespace.model.Order'
45655 * ]
45656 * @accessor
45657 */
45658 models: [],
45659
45660 /**
45661 * @cfg {Array} views The set of views to load for this Application. Each view is expected to exist inside the
45662 * *app/view* directory and define a class following the convention AppName.view.ViewName. For example, in the
45663 * code below, the classes *AppName.view.Users*, *AppName.view.Groups* and *AppName.view.Products* will be loaded.
45664 * Note that we are able to specify either the full class name (as with *AppName.view.Products*) or just the
45665 * final part of the class name and leave Application to automatically prepend *AppName.view.* to each:
45666 *
45667 * views: [
45668 * 'Users',
45669 * 'Groups',
45670 * 'AppName.view.Products',
45671 * 'SomeCustomNamespace.view.Orders'
45672 * ]
45673 * @accessor
45674 */
45675 views: []
45676 },
45677
45678 /**
45679 * Constructs a new Controller instance
45680 */
45681 constructor: function(config) {
45682 this.initConfig(config);
45683
45684 this.mixins.observable.constructor.call(this, config);
45685 },
45686
45687 /**
45688 * @cfg
45689 * Called by the Controller's {@link #application} to initialize the Controller. This is always called before the
45690 * {@link Ext.app.Application Application} launches, giving the Controller a chance to run any pre-launch logic.
45691 * See also {@link #launch}, which is called after the {@link Ext.app.Application#launch Application's launch function}
45692 */
45693 init: Ext.emptyFn,
45694
45695 /**
45696 * @cfg
45697 * Called by the Controller's {@link #application} immediately after the Application's own
45698 * {@link Ext.app.Application#launch launch function} has been called. This is usually a good place to run any
45699 * logic that has to run after the app UI is initialized. See also {@link #init}, which is called before the
45700 * {@link Ext.app.Application#launch Application's launch function}.
45701 */
45702 launch: Ext.emptyFn,
45703
45704 /**
45705 * Convenient way to redirect to a new url. See {@link Ext.app.Application#redirectTo} for full usage information.
45706 * @return {Object}
45707 */
45708 redirectTo: function(place) {
45709 return this.getApplication().redirectTo(place);
45710 },
45711
45712 /**
45713 * @private
45714 * Executes an Ext.app.Action by giving it the correct before filters and kicking off execution
45715 */
45716 execute: function(action, skipFilters) {
45717 action.setBeforeFilters(this.getBefore()[action.getAction()]);
45718 action.execute();
45719 },
45720
45721 /**
45722 * @private
45723 * Massages the before filters into an array of function references for each controller action
45724 */
45725 applyBefore: function(before) {
45726 var filters, name, length, i;
45727
45728 for (name in before) {
45729 filters = Ext.Array.from(before[name]);
45730 length = filters.length;
45731
45732 for (i = 0; i < length; i++) {
45733 filters[i] = this[filters[i]];
45734 }
45735
45736 before[name] = filters;
45737 }
45738
45739 return before;
45740 },
45741
45742 /**
45743 * @private
45744 */
45745 applyControl: function(config) {
45746 this.control(config, this);
45747
45748 return config;
45749 },
45750
45751 /**
45752 * @private
45753 */
45754 applyRefs: function(refs) {
45755 //<debug>
45756 if (Ext.isArray(refs)) {
45757 Ext.Logger.deprecate("In Sencha Touch 2 the refs config accepts an object but you have passed it an array.");
45758 }
45759 //</debug>
45760
45761 this.ref(refs);
45762
45763 return refs;
45764 },
45765
45766 /**
45767 * @private
45768 * Adds any routes specified in this Controller to the global Application router
45769 */
45770 applyRoutes: function(routes) {
45771 var app = this instanceof Ext.app.Application ? this : this.getApplication(),
45772 router = app.getRouter(),
45773 route, url, config;
45774
45775 for (url in routes) {
45776 route = routes[url];
45777
45778 config = {
45779 controller: this.$className
45780 };
45781
45782 if (Ext.isString(route)) {
45783 config.action = route;
45784 } else {
45785 Ext.apply(config, route);
45786 }
45787
45788 router.connect(url, config);
45789 }
45790
45791 return routes;
45792 },
45793
45794 /**
45795 * @private
45796 * As a convenience developers can locally qualify store names (e.g. 'MyStore' vs
45797 * 'MyApp.store.MyStore'). This just makes sure everything ends up fully qualified
45798 */
45799 applyStores: function(stores) {
45800 return this.getFullyQualified(stores, 'store');
45801 },
45802
45803 /**
45804 * @private
45805 * As a convenience developers can locally qualify model names (e.g. 'MyModel' vs
45806 * 'MyApp.model.MyModel'). This just makes sure everything ends up fully qualified
45807 */
45808 applyModels: function(models) {
45809 return this.getFullyQualified(models, 'model');
45810 },
45811
45812 /**
45813 * @private
45814 * As a convenience developers can locally qualify view names (e.g. 'MyView' vs
45815 * 'MyApp.view.MyView'). This just makes sure everything ends up fully qualified
45816 */
45817 applyViews: function(views) {
45818 return this.getFullyQualified(views, 'view');
45819 },
45820
45821 /**
45822 * @private
45823 * Returns the fully qualified name for any class name variant. This is used to find the FQ name for the model,
45824 * view, controller, store and profiles listed in a Controller or Application.
45825 * @param {String[]} items The array of strings to get the FQ name for
45826 * @param {String} namespace If the name happens to be an application class, add it to this namespace
45827 * @return {String} The fully-qualified name of the class
45828 */
45829 getFullyQualified: function(items, namespace) {
45830 var length = items.length,
45831 appName = this.getApplication().getName(),
45832 name, i;
45833
45834 for (i = 0; i < length; i++) {
45835 name = items[i];
45836
45837 //we check name === appName to allow MyApp.profile.MyApp to exist
45838 if (Ext.isString(name) && (Ext.Loader.getPrefix(name) === "" || name === appName)) {
45839 items[i] = appName + '.' + namespace + '.' + name;
45840 }
45841 }
45842
45843 return items;
45844 },
45845
45846 /**
45847 * @private
45848 */
45849 control: function(selectors) {
45850 this.getApplication().control(selectors, this);
45851 },
45852
45853 /**
45854 * @private
45855 * 1.x-inspired ref implementation
45856 */
45857 ref: function(refs) {
45858 var me = this,
45859 refName, getterName, selector, info;
45860
45861 for (refName in refs) {
45862 selector = refs[refName];
45863 getterName = "get" + Ext.String.capitalize(refName);
45864
45865 if (!this[getterName]) {
45866 if (Ext.isString(refs[refName])) {
45867 info = {
45868 ref: refName,
45869 selector: selector
45870 };
45871 } else {
45872 info = refs[refName];
45873 }
45874
45875 this[getterName] = function(refName, info) {
45876 var args = [refName, info];
45877 return function() {
45878 return me.getRef.apply(me, args.concat.apply(args, arguments));
45879 };
45880 }(refName, info);
45881 }
45882
45883 this.references = this.references || [];
45884 this.references.push(refName.toLowerCase());
45885 }
45886 },
45887
45888 /**
45889 * @private
45890 */
45891 getRef: function(ref, info, config) {
45892 this.refCache = this.refCache || {};
45893 info = info || {};
45894 config = config || {};
45895
45896 Ext.apply(info, config);
45897
45898 if (info.forceCreate) {
45899 return Ext.ComponentManager.create(info, 'component');
45900 }
45901
45902 var me = this,
45903 cached = me.refCache[ref];
45904
45905 if (!cached) {
45906 me.refCache[ref] = cached = Ext.ComponentQuery.query(info.selector)[0];
45907 if (!cached && info.autoCreate) {
45908 me.refCache[ref] = cached = Ext.ComponentManager.create(info, 'component');
45909 }
45910 if (cached) {
45911 cached.on('destroy', function() {
45912 me.refCache[ref] = null;
45913 });
45914 }
45915 }
45916
45917 return cached;
45918 },
45919
45920 /**
45921 * @private
45922 */
45923 hasRef: function(ref) {
45924 return this.references && this.references.indexOf(ref.toLowerCase()) !== -1;
45925 }
45926
45927 }, function() {
45928 });
45929
45930 /**
45931 * @author Ed Spencer
45932 * @private
45933 *
45934 * Manages the stack of {@link Ext.app.Action} instances that have been decoded, pushes new urls into the browser's
45935 * location object and listens for changes in url, firing the {@link #change} event when a change is detected.
45936 *
45937 * This is tied to an {@link Ext.app.Application Application} instance. The Application performs all of the
45938 * interactions with the History object, no additional integration should be required.
45939 */
45940 Ext.define('Ext.app.History', {
45941 mixins: [ Ext.mixin.Observable ],
45942
45943 /**
45944 * @event change
45945 * Fires when a change in browser url is detected
45946 * @param {String} url The new url, after the hash (e.g. http://myapp.com/#someUrl returns 'someUrl')
45947 */
45948
45949 config: {
45950 /**
45951 * @cfg {Array} actions The stack of {@link Ext.app.Action action} instances that have occurred so far
45952 */
45953 actions: [],
45954
45955 /**
45956 * @cfg {Boolean} updateUrl `true` to automatically update the browser's url when {@link #add} is called.
45957 */
45958 updateUrl: true,
45959
45960 /**
45961 * @cfg {String} token The current token as read from the browser's location object.
45962 */
45963 token: ''
45964 },
45965
45966 constructor: function(config) {
45967 if (Ext.feature.has.History) {
45968 window.addEventListener('hashchange', Ext.bind(this.detectStateChange, this));
45969 }
45970 else {
45971 setInterval(Ext.bind(this.detectStateChange, this), 100);
45972 }
45973
45974 this.initConfig(config);
45975 if (config && Ext.isEmpty(config.token)) {
45976 this.setToken(window.location.hash.substr(1));
45977 }
45978 },
45979
45980 /**
45981 * Adds an {@link Ext.app.Action Action} to the stack, optionally updating the browser's url and firing the
45982 * {@link #change} event.
45983 * @param {Ext.app.Action} action The Action to add to the stack.
45984 * @param {Boolean} silent Cancels the firing of the {@link #change} event if `true`.
45985 */
45986 add: function(action, silent) {
45987 action = Ext.factory(action, Ext.app.Action);
45988
45989 this.getActions().push(action);
45990
45991 var url = action.getUrl();
45992
45993 if (this.getUpdateUrl()) {
45994 // history.pushState({}, action.getTitle(), "#" + action.getUrl());
45995 this.setToken(url);
45996 window.location.hash = url;
45997 }
45998
45999 if (silent !== true) {
46000 this.fireEvent('change', url);
46001 }
46002
46003 this.setToken(url);
46004 },
46005
46006 /**
46007 * Navigate to the previous active action. This changes the page url.
46008 */
46009 back: function() {
46010 var actions = this.getActions(),
46011 previousAction = actions[actions.length - 2];
46012
46013 if (previousAction) {
46014 actions.pop();
46015
46016 previousAction.getController().getApplication().redirectTo(previousAction.getUrl());
46017 }
46018 else {
46019 actions[actions.length - 1].getController().getApplication().redirectTo('');
46020 }
46021 },
46022
46023 /**
46024 * @private
46025 */
46026 applyToken: function(token) {
46027 return token[0] == '#' ? token.substr(1) : token;
46028 },
46029
46030 /**
46031 * @private
46032 */
46033 detectStateChange: function() {
46034 var newToken = this.applyToken(window.location.hash),
46035 oldToken = this.getToken();
46036
46037 if (newToken != oldToken) {
46038 this.onStateChange();
46039 this.setToken(newToken);
46040 }
46041 },
46042
46043 /**
46044 * @private
46045 */
46046 onStateChange: function() {
46047 this.fireEvent('change', window.location.hash.substr(1));
46048 }
46049 });
46050
46051 /**
46052 * @author Ed Spencer
46053 *
46054 * A Profile represents a range of devices that fall under a common category. For the vast majority of apps that use
46055 * device profiles, the app defines a Phone profile and a Tablet profile. Doing this enables you to easily customize
46056 * the experience for the different sized screens offered by those device types.
46057 *
46058 * Only one Profile can be active at a time, and each Profile defines a simple {@link #isActive} function that should
46059 * return either true or false. The first Profile to return true from its isActive function is set as your Application's
46060 * {@link Ext.app.Application#currentProfile current profile}.
46061 *
46062 * A Profile can define any number of {@link #models}, {@link #views}, {@link #controllers} and {@link #stores} which
46063 * will be loaded if the Profile is activated. It can also define a {@link #launch} function that will be called after
46064 * all of its dependencies have been loaded, just before the {@link Ext.app.Application#launch application launch}
46065 * function is called.
46066 *
46067 * ## Sample Usage
46068 *
46069 * First you need to tell your Application about your Profile(s):
46070 *
46071 * Ext.application({
46072 * name: 'MyApp',
46073 * profiles: ['Phone', 'Tablet']
46074 * });
46075 *
46076 * This will load app/profile/Phone.js and app/profile/Tablet.js. Here's how we might define the Phone profile:
46077 *
46078 * Ext.define('MyApp.profile.Phone', {
46079 * extend: 'Ext.app.Profile',
46080 *
46081 * views: ['Main'],
46082 *
46083 * isActive: function() {
46084 * return Ext.os.is('Phone');
46085 * }
46086 * });
46087 *
46088 * The isActive function returns true if we detect that we are running on a phone device. If that is the case the
46089 * Application will set this Profile active and load the 'Main' view specified in the Profile's {@link #views} config.
46090 *
46091 * ## Class Specializations
46092 *
46093 * Because Profiles are specializations of an application, all of the models, views, controllers and stores defined
46094 * in a Profile are expected to be namespaced under the name of the Profile. Here's an expanded form of the example
46095 * above:
46096 *
46097 * Ext.define('MyApp.profile.Phone', {
46098 * extend: 'Ext.app.Profile',
46099 *
46100 * views: ['Main'],
46101 * controllers: ['Signup'],
46102 * models: ['MyApp.model.Group'],
46103 *
46104 * isActive: function() {
46105 * return Ext.os.is('Phone');
46106 * }
46107 * });
46108 *
46109 * In this case, the Profile is going to load *app/view/phone/Main.js*, *app/controller/phone/Signup.js* and
46110 * *app/model/Group.js*. Notice that in each of the first two cases the name of the profile ('phone' in this case) was
46111 * injected into the class names. In the third case we specified the full Model name (for Group) so the Profile name
46112 * was not injected.
46113 *
46114 * For a fuller understanding of the ideas behind Profiles and how best to use them in your app, we suggest you read
46115 * the [device profiles guide](../../../core_concepts/device_profiles.html).
46116 */
46117 Ext.define('Ext.app.Profile', {
46118 mixins: {
46119 observable: Ext.mixin.Observable
46120 },
46121
46122 config: {
46123 /**
46124 * @cfg {String} namespace The namespace that this Profile's classes can be found in. Defaults to the lowercased
46125 * Profile {@link #name}, for example a Profile called MyApp.profile.Phone will by default have a 'phone'
46126 * namespace, which means that this Profile's additional models, stores, views and controllers will be loaded
46127 * from the MyApp.model.phone.*, MyApp.store.phone.*, MyApp.view.phone.* and MyApp.controller.phone.* namespaces
46128 * respectively.
46129 * @accessor
46130 */
46131 namespace: 'auto',
46132
46133 /**
46134 * @cfg {String} name The name of this Profile. Defaults to the last section of the class name (e.g. a profile
46135 * called MyApp.profile.Phone will default the name to 'Phone').
46136 * @accessor
46137 */
46138 name: 'auto',
46139
46140 /**
46141 * @cfg {Array} controllers Any additional {@link Ext.app.Application#controllers Controllers} to load for this
46142 * profile. Note that each item here will be prepended with the Profile namespace when loaded. Example usage:
46143 *
46144 * controllers: [
46145 * 'Users',
46146 * 'MyApp.controller.Products'
46147 * ]
46148 *
46149 * This will load *MyApp.controller.tablet.Users* and *MyApp.controller.Products*.
46150 * @accessor
46151 */
46152 controllers: [],
46153
46154 /**
46155 * @cfg {Array} models Any additional {@link Ext.app.Application#models Models} to load for this profile. Note
46156 * that each item here will be prepended with the Profile namespace when loaded. Example usage:
46157 *
46158 * models: [
46159 * 'Group',
46160 * 'MyApp.model.User'
46161 * ]
46162 *
46163 * This will load *MyApp.model.tablet.Group* and *MyApp.model.User*.
46164 * @accessor
46165 */
46166 models: [],
46167
46168 /**
46169 * @cfg {Array} views Any additional {@link Ext.app.Application#views views} to load for this profile. Note
46170 * that each item here will be prepended with the Profile namespace when loaded. Example usage:
46171 *
46172 * views: [
46173 * 'Main',
46174 * 'MyApp.view.Login'
46175 * ]
46176 *
46177 * This will load *MyApp.view.tablet.Main* and *MyApp.view.Login*.
46178 * @accessor
46179 */
46180 views: [],
46181
46182 /**
46183 * @cfg {Array} stores Any additional {@link Ext.app.Application#stores Stores} to load for this profile. Note
46184 * that each item here will be prepended with the Profile namespace when loaded. Example usage:
46185 *
46186 * stores: [
46187 * 'Users',
46188 * 'MyApp.store.Products'
46189 * ]
46190 *
46191 * This will load *MyApp.store.tablet.Users* and *MyApp.store.Products*.
46192 * @accessor
46193 */
46194 stores: [],
46195
46196 /**
46197 * @cfg {Ext.app.Application} application The {@link Ext.app.Application Application} instance that this
46198 * Profile is bound to. This is set automatically.
46199 * @accessor
46200 * @readonly
46201 */
46202 application: null
46203 },
46204
46205 /**
46206 * Creates a new Profile instance
46207 */
46208 constructor: function(config) {
46209 this.initConfig(config);
46210
46211 this.mixins.observable.constructor.apply(this, arguments);
46212 },
46213
46214 /**
46215 * Determines whether or not this Profile is active on the device isActive is executed on. Should return true if
46216 * this profile is meant to be active on this device, false otherwise. Each Profile should implement this function
46217 * (the default implementation just returns false).
46218 * @return {Boolean} True if this Profile should be activated on the device it is running on, false otherwise
46219 */
46220 isActive: function() {
46221 return false;
46222 },
46223
46224 /**
46225 * @method
46226 * The launch function is called by the {@link Ext.app.Application Application} if this Profile's {@link #isActive}
46227 * function returned true. This is typically the best place to run any profile-specific app launch code. Example
46228 * usage:
46229 *
46230 * launch: function() {
46231 * Ext.create('MyApp.view.tablet.Main');
46232 * }
46233 */
46234 launch: Ext.emptyFn,
46235
46236 /**
46237 * @private
46238 */
46239 applyNamespace: function(name) {
46240 if (name == 'auto') {
46241 name = this.getName();
46242 }
46243
46244 return name.toLowerCase();
46245 },
46246
46247 /**
46248 * @private
46249 */
46250 applyName: function(name) {
46251 if (name == 'auto') {
46252 var pieces = this.$className.split('.');
46253 name = pieces[pieces.length - 1];
46254 }
46255
46256 return name;
46257 },
46258
46259 /**
46260 * @private
46261 * Computes the full class names of any specified model, view, controller and store dependencies, returns them in
46262 * an object map for easy loading
46263 */
46264 getDependencies: function() {
46265 var allClasses = [],
46266 format = Ext.String.format,
46267 appName = this.getApplication().getName(),
46268 namespace = this.getNamespace(),
46269 map = {
46270 model: this.getModels(),
46271 view: this.getViews(),
46272 controller: this.getControllers(),
46273 store: this.getStores()
46274 },
46275 classType, classNames, fullyQualified;
46276
46277 for (classType in map) {
46278 classNames = [];
46279
46280 Ext.each(map[classType], function(className) {
46281 if (Ext.isString(className)) {
46282 //we check name === appName to allow MyApp.profile.MyApp to exist
46283 if (Ext.isString(className) && (Ext.Loader.getPrefix(className) === "" || className === appName)) {
46284 className = appName + '.' + classType + '.' + namespace + '.' + className;
46285 }
46286
46287 classNames.push(className);
46288 allClasses.push(className);
46289 }
46290 }, this);
46291
46292 map[classType] = classNames;
46293 }
46294
46295 map.all = allClasses;
46296
46297 return map;
46298 }
46299 });
46300
46301 /**
46302 * @author Ed Spencer
46303 * @private
46304 *
46305 * Represents a mapping between a url and a controller/action pair. May also contain additional params. This is a
46306 * private internal class that should not need to be used by end-developer code. Its API and existence are subject to
46307 * change so use at your own risk.
46308 *
46309 * For information on how to use routes we suggest reading the following guides:
46310 *
46311 * - [Using History Support](../../../core_concepts/history_support.html)
46312 * - [Intro to Applications](../../../core_concepts/about_applications.html)
46313 * - [Using Controllers](../../../core_concepts/controllers.html)
46314 *
46315 */
46316 Ext.define('Ext.app.Route', {
46317
46318 config: {
46319 /**
46320 * @cfg {Object} conditions Optional set of conditions for each token in the url string. Each key should be one
46321 * of the tokens, each value should be a regex that the token should accept. For example, if you have a Route
46322 * with a url like "files/:fileName" and you want it to match urls like "files/someImage.jpg" then you can set
46323 * these conditions to allow the :fileName token to accept strings containing a period ("."):
46324 *
46325 * conditions: {
46326 * ':fileName': "[0-9a-zA-Z\.]+"
46327 * }
46328 *
46329 */
46330 conditions: {},
46331
46332 /**
46333 * @cfg {String} url (required) The url regex to match against.
46334 */
46335 url: null,
46336
46337 /**
46338 * @cfg {String} controller The name of the Controller whose {@link #action} will be called if this route is
46339 * matched.
46340 */
46341 controller: null,
46342
46343 /**
46344 * @cfg {String} action The name of the action that will be called on the {@link #controller} if this route is
46345 * matched.
46346 */
46347 action: null,
46348
46349 /**
46350 * @private
46351 * @cfg {Boolean} initialized Indicates whether or not this Route has been initialized. We don't initialize
46352 * straight away so as to save unnecessary processing.
46353 */
46354 initialized: false
46355 },
46356
46357 constructor: function(config) {
46358 this.initConfig(config);
46359 },
46360
46361 /**
46362 * Attempts to recognize a given url string and return controller/action pair for it.
46363 * @param {String} url The url to recognize.
46364 * @return {Object/Boolean} The matched data, or `false` if no match.
46365 */
46366 recognize: function(url) {
46367 if (!this.getInitialized()) {
46368 this.initialize();
46369 }
46370
46371 if (this.recognizes(url)) {
46372 var matches = this.matchesFor(url),
46373 args = url.match(this.matcherRegex);
46374
46375 args.shift();
46376
46377 return Ext.applyIf(matches, {
46378 controller: this.getController(),
46379 action : this.getAction(),
46380 url : url,
46381 args : args,
46382
46383 // We keep the historyUrl in here for backwards compatibility
46384 historyUrl: url
46385 });
46386 }
46387 },
46388
46389 /**
46390 * @private
46391 * Sets up the relevant regular expressions used to match against this route.
46392 */
46393 initialize: function() {
46394 /*
46395 * The regular expression we use to match a segment of a route mapping
46396 * this will recognize segments starting with a colon,
46397 * e.g. on 'namespace/:controller/:action', :controller and :action will be recognized
46398 */
46399 this.paramMatchingRegex = new RegExp(/:([0-9A-Za-z\_]*)/g);
46400
46401 /*
46402 * Converts a route string into an array of symbols starting with a colon. e.g.
46403 * ":controller/:action/:id" => [':controller', ':action', ':id']
46404 */
46405 this.paramsInMatchString = this.getUrl().match(this.paramMatchingRegex) || [];
46406
46407 this.matcherRegex = this.createMatcherRegex(this.getUrl());
46408
46409 this.setInitialized(true);
46410 },
46411
46412 /**
46413 * @private
46414 * Returns true if this Route matches the given url string
46415 * @param {String} url The url to test
46416 * @return {Boolean} True if this Route recognizes the url
46417 */
46418 recognizes: function(url) {
46419 return this.matcherRegex.test(url);
46420 },
46421
46422 /**
46423 * @private
46424 * Returns a hash of matching url segments for the given url.
46425 * @param {String} url The url to extract matches for
46426 * @return {Object} matching url segments
46427 */
46428 matchesFor: function(url) {
46429 var params = {},
46430 keys = this.paramsInMatchString,
46431 values = url.match(this.matcherRegex),
46432 length = keys.length,
46433 i;
46434
46435 //first value is the entire match so reject
46436 values.shift();
46437
46438 for (i = 0; i < length; i++) {
46439 params[keys[i].replace(":", "")] = values[i];
46440 }
46441
46442 return params;
46443 },
46444
46445 /**
46446 * @private
46447 * Returns an array of matching url segments for the given url.
46448 * @param {String} url The url to extract matches for
46449 * @return {Array} matching url segments
46450 */
46451 argsFor: function(url) {
46452 var args = [],
46453 keys = this.paramsInMatchString,
46454 values = url.match(this.matcherRegex),
46455 length = keys.length,
46456 i;
46457
46458 //first value is the entire match so reject
46459 values.shift();
46460
46461 for (i = 0; i < length; i++) {
46462 args.push(keys[i].replace(':', ""));
46463 params[keys[i].replace(":", "")] = values[i];
46464 }
46465
46466 return params;
46467 },
46468
46469 /**
46470 * @private
46471 * Constructs a url for the given config object by replacing wildcard placeholders in the Route's url
46472 * @param {Object} config The config object
46473 * @return {String} The constructed url
46474 */
46475 urlFor: function(config) {
46476 var url = this.getUrl();
46477
46478 for (var key in config) {
46479 url = url.replace(":" + key, config[key]);
46480 }
46481
46482 return url;
46483 },
46484
46485 /**
46486 * @private
46487 * Takes the configured url string including wildcards and returns a regex that can be used to match
46488 * against a url
46489 * @param {String} url The url string
46490 * @return {RegExp} The matcher regex
46491 */
46492 createMatcherRegex: function(url) {
46493 /**
46494 * Converts a route string into an array of symbols starting with a colon. e.g.
46495 * ":controller/:action/:id" => [':controller', ':action', ':id']
46496 */
46497 var paramsInMatchString = this.paramsInMatchString,
46498 length = paramsInMatchString.length,
46499 i, cond, matcher;
46500
46501 for (i = 0; i < length; i++) {
46502 cond = this.getConditions()[paramsInMatchString[i]];
46503 matcher = Ext.util.Format.format("({0})", cond || "[%a-zA-Z0-9\\-\\_\\s,]+");
46504
46505 url = url.replace(new RegExp(paramsInMatchString[i]), matcher);
46506 }
46507
46508 //we want to match the whole string, so include the anchors
46509 return new RegExp("^" + url + "$");
46510 }
46511 });
46512
46513 /**
46514 * @author Ed Spencer
46515 * @private
46516 *
46517 * The Router is an ordered set of route definitions that decode a url into a controller function to execute. Each
46518 * route defines a type of url to match, along with the controller function to call if it is matched. The Router is
46519 * usually managed exclusively by an {@link Ext.app.Application Application}, which also uses a
46520 * {@link Ext.app.History History} instance to find out when the browser's url has changed.
46521 *
46522 * Routes are almost always defined inside a {@link Ext.app.Controller Controller}, as opposed to on the Router itself.
46523 * End-developers should not usually need to interact directly with the Router as the Application and Controller
46524 * classes manage everything automatically. See the {@link Ext.app.Controller Controller documentation} for more
46525 * information on specifying routes.
46526 */
46527 Ext.define('Ext.app.Router', {
46528
46529
46530 config: {
46531 /**
46532 * @cfg {Array} routes The set of routes contained within this Router.
46533 * @readonly
46534 */
46535 routes: [],
46536
46537 /**
46538 * @cfg {Object} defaults Default configuration options for each Route connected to this Router.
46539 */
46540 defaults: {
46541 action: 'index'
46542 }
46543 },
46544
46545 constructor: function(config) {
46546 this.initConfig(config);
46547 },
46548
46549 /**
46550 * Connects a url-based route to a controller/action pair plus additional params.
46551 * @param {String} url The url to recognize.
46552 * @param {Object} [params] Additional parameters.
46553 */
46554 connect: function(url, params) {
46555 params = Ext.apply({url: url}, params || {}, this.getDefaults());
46556 var route = Ext.create('Ext.app.Route', params);
46557
46558 this.getRoutes().push(route);
46559
46560 return route;
46561 },
46562
46563 /**
46564 * Recognizes a url string connected to the Router, return the controller/action pair plus any additional
46565 * config associated with it.
46566 * @param {String} url The url to recognize.
46567 * @return {Object/undefined} If the url was recognized, the controller and action to call, else `undefined`.
46568 */
46569 recognize: function(url) {
46570 var routes = this.getRoutes(),
46571 length = routes.length,
46572 i, result;
46573
46574 for (i = 0; i < length; i++) {
46575 result = routes[i].recognize(url);
46576
46577 if (result !== undefined) {
46578 return result;
46579 }
46580 }
46581
46582 return undefined;
46583 },
46584
46585 /**
46586 * Convenience method which just calls the supplied function with the Router instance. Example usage:
46587 *
46588 * Ext.Router.draw(function(map) {
46589 * map.connect('activate/:token', {controller: 'users', action: 'activate'});
46590 * map.connect('home', {controller: 'index', action: 'home'});
46591 * });
46592 *
46593 * @param {Function} fn The fn to call
46594 */
46595 draw: function(fn) {
46596 fn.call(this, this);
46597 },
46598
46599 /**
46600 * @private
46601 */
46602 clear: function() {
46603 this.setRoutes([]);
46604 }
46605 }, function() {
46606 });
46607
46608 /**
46609 * @author Ed Spencer
46610 *
46611 * Ext.app.Application defines the set of {@link Ext.data.Model Models}, {@link Ext.app.Controller Controllers},
46612 * {@link Ext.app.Profile Profiles}, {@link Ext.data.Store Stores} and {@link Ext.Component Views} that an application
46613 * consists of. It automatically loads all of those dependencies and can optionally specify a {@link #launch} function
46614 * that will be called when everything is ready.
46615 *
46616 * Sample usage:
46617 *
46618 * Ext.application({
46619 * name: 'MyApp',
46620 *
46621 * models: ['User', 'Group'],
46622 * stores: ['Users'],
46623 * controllers: ['Users'],
46624 * views: ['Main', 'ShowUser'],
46625 *
46626 * launch: function() {
46627 * Ext.create('MyApp.view.Main');
46628 * }
46629 * });
46630 *
46631 * Creating an Application instance is the only time in Sencha Touch that we don't use Ext.create to create the new
46632 * instance. Instead, the {@link Ext#application} function instantiates an Ext.app.Application internally,
46633 * automatically loading the Ext.app.Application class if it is not present on the page already and hooking in to
46634 * {@link Ext#onReady} before creating the instance itself. An alternative is to use Ext.create inside an Ext.onReady
46635 * callback, but Ext.application is preferred.
46636 *
46637 * ## Dependencies
46638 *
46639 * Application follows a simple convention when it comes to specifying the controllers, views, models, stores and
46640 * profiles it requires. By default it expects each of them to be found inside the *app/controller*, *app/view*,
46641 * *app/model*, *app/store* and *app/profile* directories in your app - if you follow this convention you can just
46642 * specify the last part of each class name and Application will figure out the rest for you:
46643 *
46644 * Ext.application({
46645 * name: 'MyApp',
46646 *
46647 * controllers: ['Users'],
46648 * models: ['User', 'Group'],
46649 * stores: ['Users'],
46650 * views: ['Main', 'ShowUser']
46651 * });
46652 *
46653 * The example above will load 6 files:
46654 *
46655 * - app/model/User.js
46656 * - app/model/Group.js
46657 * - app/store/Users.js
46658 * - app/controller/Users.js
46659 * - app/view/Main.js
46660 * - app/view/ShowUser.js
46661 *
46662 * ### Nested Dependencies
46663 *
46664 * For larger apps it's common to split the models, views and controllers into subfolders to keep the project
46665 * organized. This is especially true of views - it's not unheard of for large apps to have over a hundred separate
46666 * view classes so organizing them into folders can make maintenance much simpler.
46667 *
46668 * To specify dependencies in subfolders just use a period (".") to specify the folder:
46669 *
46670 * Ext.application({
46671 * name: 'MyApp',
46672 *
46673 * controllers: ['Users', 'nested.MyController'],
46674 * views: ['products.Show', 'products.Edit', 'user.Login']
46675 * });
46676 *
46677 * In this case these 5 files will be loaded:
46678 *
46679 * - app/controller/Users.js
46680 * - app/controller/nested/MyController.js
46681 * - app/view/products/Show.js
46682 * - app/view/products/Edit.js
46683 * - app/view/user/Login.js
46684 *
46685 * Note that we can mix and match within each configuration here - for each model, view, controller, profile or store
46686 * you can specify either just the final part of the class name (if you follow the directory conventions), or the full
46687 * class name.
46688 *
46689 * ### External Dependencies
46690 *
46691 * Finally, we can specify application dependencies from outside our application by fully-qualifying the classes we
46692 * want to load. A common use case for this is sharing authentication logic between multiple applications. Perhaps you
46693 * have several apps that login via a common user database and you want to share that code between them. An easy way to
46694 * do this is to create a folder alongside your app folder and then add its contents as dependencies for your app.
46695 *
46696 * For example, let's say our shared login code contains a login controller, a user model and a login form view. We
46697 * want to use all of these in our application:
46698 *
46699 * Ext.Loader.setPath({
46700 * 'Auth': 'Auth'
46701 * });
46702 *
46703 * Ext.application({
46704 * views: ['Auth.view.LoginForm', 'Welcome'],
46705 * controllers: ['Auth.controller.Sessions', 'Main'],
46706 * models: ['Auth.model.User']
46707 * });
46708 *
46709 * This will load the following files:
46710 *
46711 * - Auth/view/LoginForm.js
46712 * - Auth/controller/Sessions.js
46713 * - Auth/model/User.js
46714 * - app/view/Welcome.js
46715 * - app/controller/Main.js
46716 *
46717 * The first three were loaded from outside our application, the last two from the application itself. Note how we can
46718 * still mix and match application files and external dependency files.
46719 *
46720 * Note that to enable the loading of external dependencies we just have to tell the Loader where to find those files,
46721 * which is what we do with the Ext.Loader.setPath call above. In this case we're telling the Loader to find any class
46722 * starting with the 'Auth' namespace inside our 'Auth' folder. This means we can drop our common Auth code into our
46723 * application alongside the app folder and the framework will be able to figure out how to load everything.
46724 *
46725 * ## Launching
46726 *
46727 * Each Application can define a {@link Ext.app.Application#launch launch} function, which is called as soon as all of
46728 * your app's classes have been loaded and the app is ready to be launched. This is usually the best place to put any
46729 * application startup logic, typically creating the main view structure for your app.
46730 *
46731 * In addition to the Application launch function, there are two other places you can put app startup logic. Firstly,
46732 * each Controller is able to define an {@link Ext.app.Controller#init init} function, which is called before the
46733 * Application launch function. Secondly, if you are using Device Profiles, each Profile can define a
46734 * {@link Ext.app.Profile#launch launch} function, which is called after the Controller init functions but before the
46735 * Application launch function.
46736 *
46737 * Note that only the active Profile has its launch function called - for example if you define profiles for Phone and
46738 * Tablet and then launch the app on a tablet, only the Tablet Profile's launch function is called.
46739 *
46740 * 1. Controller#init functions called
46741 * 2. Profile#launch function called
46742 * 3. Application#launch function called
46743 * 4. Controller#launch functions called
46744 *
46745 * When using Profiles it is common to place most of the bootup logic inside the Profile launch function because each
46746 * Profile has a different set of views that need to be constructed at startup.
46747 *
46748 * ## Adding to Home Screen
46749 *
46750 * iOS devices allow your users to add your app to their home screen for easy access. iOS allows you to customize
46751 * several aspects of this, including the icon that will appear on the home screen and the startup image. These can be
46752 * specified in the Ext.application setup block:
46753 *
46754 * Ext.application({
46755 * name: 'MyApp',
46756 *
46757 * {@link #icon}: 'resources/img/icon.png',
46758 * {@link #isIconPrecomposed}: false,
46759 * {@link #startupImage}: {
46760 * '320x460': 'resources/startup/320x460.jpg',
46761 * '640x920': 'resources/startup/640x920.png',
46762 * '640x1096': 'resources/startup/640x1096.png',
46763 * '768x1004': 'resources/startup/768x1004.png',
46764 * '748x1024': 'resources/startup/748x1024.png',
46765 * '1536x2008': 'resources/startup/1536x2008.png',
46766 * '1496x2048': 'resources/startup/1496x2048.png'
46767 * }
46768 * });
46769 *
46770 * When the user adds your app to the home screen, your resources/img/icon.png file will be used as the application
46771 * {@link #icon}. We also used the {@link #isIconPrecomposed} configuration to turn off the gloss effect that is automatically added
46772 * to icons in iOS. Finally we used the {@link #startupImage} configuration to provide the images that will be displayed
46773 * while your application is starting up. See also {@link #statusBarStyle}.
46774 *
46775 * ## Find out more
46776 *
46777 * If you are not already familiar with writing applications with Sencha Touch we recommend reading the
46778 * [intro to applications guide](../../../getting_started/building_your_first_app.html), which
46779 * lays out the core principles of writing apps with Sencha Touch.
46780 */
46781 Ext.define('Ext.app.Application', {
46782 extend: Ext.app.Controller ,
46783
46784
46785
46786
46787
46788
46789
46790
46791 config: {
46792 /**
46793 * @cfg {String/Object} icon
46794 * Specifies a set of URLs to the application icon for different device form factors. This icon is displayed
46795 * when the application is added to the device's Home Screen.
46796 *
46797 * Ext.setup({
46798 * icon: {
46799 * 57: 'resources/icons/Icon.png',
46800 * 72: 'resources/icons/Icon~ipad.png',
46801 * 114: 'resources/icons/Icon@2x.png',
46802 * 144: 'resources/icons/Icon~ipad@2x.png'
46803 * },
46804 * onReady: function() {
46805 * // ...
46806 * }
46807 * });
46808 *
46809 * Each key represents the dimension of the icon as a square shape. For example: '57' is the key for a 57 x 57
46810 * icon image. Here is the breakdown of each dimension and its device target:
46811 *
46812 * - 57: Non-retina iPhone, iPod touch, and all Android devices
46813 * - 72: Retina iPhone and iPod touch
46814 * - 114: Non-retina iPad (first and second generation)
46815 * - 144: Retina iPad (third generation)
46816 *
46817 * Note that the dimensions of the icon images must be exactly 57x57, 72x72, 114x114 and 144x144 respectively.
46818 *
46819 * It is highly recommended that you provide all these different sizes to accommodate a full range of
46820 * devices currently available. However if you only have one icon in one size, make it 57x57 in size and
46821 * specify it as a string value. This same icon will be used on all supported devices.
46822 *
46823 * Ext.application({
46824 * icon: 'resources/icons/Icon.png',
46825 * launch: function() {
46826 * // ...
46827 * }
46828 * });
46829 */
46830
46831 /**
46832 * @cfg {Object} startupImage
46833 * Specifies a set of URLs to the application startup images for different device form factors. This image is
46834 * displayed when the application is being launched from the Home Screen icon. Note that this currently only applies
46835 * to iOS devices.
46836 *
46837 * Ext.application({
46838 * startupImage: {
46839 * '320x460': 'resources/startup/320x460.jpg',
46840 * '640x920': 'resources/startup/640x920.png',
46841 * '640x1096': 'resources/startup/640x1096.png',
46842 * '768x1004': 'resources/startup/768x1004.png',
46843 * '748x1024': 'resources/startup/748x1024.png',
46844 * '1536x2008': 'resources/startup/1536x2008.png',
46845 * '1496x2048': 'resources/startup/1496x2048.png'
46846 * },
46847 * launch: function() {
46848 * // ...
46849 * }
46850 * });
46851 *
46852 * Each key represents the dimension of the image. For example: '320x460' is the key for a 320px x 460px image.
46853 * Here is the breakdown of each dimension and its device target:
46854 *
46855 * - 320x460: Non-retina iPhone, iPod touch, and all Android devices
46856 * - 640x920: Retina iPhone and iPod touch
46857 * - 640x1096: iPhone 5 and iPod touch (fifth generation)
46858 * - 768x1004: Non-retina iPad (first and second generation) in portrait orientation
46859 * - 748x1024: Non-retina iPad (first and second generation) in landscape orientation
46860 * - 1536x2008: Retina iPad (third generation) in portrait orientation
46861 * - 1496x2048: Retina iPad (third generation) in landscape orientation
46862 *
46863 * Please note that there's no automatic fallback mechanism for the startup images. In other words, if you don't specify
46864 * a valid image for a certain device, nothing will be displayed while the application is being launched on that device.
46865 */
46866
46867 /**
46868 * @cfg {Boolean} isIconPrecomposed
46869 * `true` to not having a glossy effect added to the icon by the OS, which will preserve its exact look. This currently
46870 * only applies to iOS devices.
46871 */
46872
46873 /**
46874 * @cfg {String} [statusBarStyle='black'] Allows you to set the style of the status bar when your app is added to the
46875 * home screen on iOS devices. Alternative is to set to 'black-translucent', which turns
46876 * the status bar semi-transparent and overlaps the app content. This is usually not a good option for web apps
46877 */
46878
46879 /**
46880 * @cfg {String} tabletIcon Path to the _.png_ image file to use when your app is added to the home screen on an
46881 * iOS **tablet** device (iPad).
46882 * @deprecated 2.0.0 Please use the {@link #icon} configuration instead.
46883 */
46884
46885 /**
46886 * @cfg {String} phoneIcon Path to the _.png_ image file to use when your app is added to the home screen on an
46887 * iOS **phone** device (iPhone or iPod).
46888 * @deprecated 2.0.0 Please use the {@link #icon} configuration instead.
46889 */
46890
46891 /**
46892 * @cfg {Boolean} glossOnIcon If set to `false`, the 'gloss' effect added to home screen {@link #icon icons} on
46893 * iOS devices will be removed.
46894 * @deprecated 2.0.0 Please use the {@link #isIconPrecomposed} configuration instead.
46895 */
46896
46897 /**
46898 * @cfg {String} phoneStartupScreen Path to the _.png_ image file that will be displayed while the app is
46899 * starting up once it has been added to the home screen of an iOS phone device (iPhone or iPod). This _.png_
46900 * file should be 320px wide and 460px high.
46901 * @deprecated 2.0.0 Please use the {@link #startupImage} configuration instead.
46902 */
46903
46904 /**
46905 * @cfg {String} tabletStartupScreen Path to the _.png_ image file that will be displayed while the app is
46906 * starting up once it has been added to the home screen of an iOS tablet device (iPad). This _.png_ file should
46907 * be 768px wide and 1004px high.
46908 * @deprecated 2.0.0 Please use the {@link #startupImage} configuration instead.
46909 */
46910
46911 /**
46912 * @cfg {Array} profiles The set of profiles to load for this Application. Each profile is expected to
46913 * exist inside the *app/profile* directory and define a class following the convention
46914 * AppName.profile.ProfileName. For example, in the code below, the classes *AppName.profile.Phone*
46915 * and *AppName.profile.Tablet* will be loaded. Note that we are able to specify
46916 * either the full class name (as with *AppName.profile.Tablet*) or just the final part of the class name
46917 * and leave Application to automatically prepend *AppName.profile.'* to each:
46918 *
46919 * profiles: [
46920 * 'Phone',
46921 * 'AppName.profile.Tablet',
46922 * 'SomeCustomNamespace.profile.Desktop'
46923 * ]
46924 * @accessor
46925 */
46926 profiles: [],
46927
46928 /**
46929 * @cfg {Array} controllers The set of controllers to load for this Application. Each controller is expected to
46930 * exist inside the *app/controller* directory and define a class following the convention
46931 * AppName.controller.ControllerName. For example, in the code below, the classes *AppName.controller.Users*,
46932 * *AppName.controller.Groups* and *AppName.controller.Products* will be loaded. Note that we are able to specify
46933 * either the full class name (as with *AppName.controller.Products*) or just the final part of the class name
46934 * and leave Application to automatically prepend *AppName.controller.'* to each:
46935 *
46936 * controllers: [
46937 * 'Users',
46938 * 'Groups',
46939 * 'AppName.controller.Products',
46940 * 'SomeCustomNamespace.controller.Orders'
46941 * ]
46942 * @accessor
46943 */
46944 controllers: [],
46945
46946 /**
46947 * @cfg {Ext.app.History} history The global {@link Ext.app.History History} instance attached to this
46948 * Application. For more information, see
46949 * [Routing, Deep Linking, and the Back Button](../../../core_concepts/history_support.html).
46950 * @accessor
46951 * @readonly
46952 */
46953 history: {},
46954
46955 /**
46956 * @cfg {String} name The name of the Application. This should be a single word without spaces or periods
46957 * because it is used as the Application's global namespace. All classes in your application should be
46958 * namespaced under the Application's name - for example if your application name is 'MyApp', your classes
46959 * should be named 'MyApp.model.User', 'MyApp.controller.Users', 'MyApp.view.Main' etc
46960 * @accessor
46961 */
46962 name: null,
46963
46964 /**
46965 * @cfg {String} appFolder The path to the directory which contains all application's classes.
46966 * This path will be registered via {@link Ext.Loader#setPath} for the namespace specified in the {@link #name name} config.
46967 * @accessor
46968 */
46969 appFolder : 'app',
46970
46971 /**
46972 * @cfg {Ext.app.Router} router The global {@link Ext.app.Router Router} instance attached to this Application.
46973 * @accessor
46974 * @readonly
46975 */
46976 router: {},
46977
46978 /**
46979 * @cfg {Array} controllerInstances Used internally as the collection of instantiated controllers. Use {@link #getController} instead.
46980 * @private
46981 * @accessor
46982 */
46983 controllerInstances: [],
46984
46985 /**
46986 * @cfg {Array} profileInstances Used internally as the collection of instantiated profiles.
46987 * @private
46988 * @accessor
46989 */
46990 profileInstances: [],
46991
46992 /**
46993 * @cfg {Ext.app.Profile} currentProfile The {@link Ext.app.Profile Profile} that is currently active for the
46994 * Application. This is set once, automatically by the Application before launch.
46995 * @accessor
46996 * @readonly
46997 */
46998 currentProfile: null,
46999
47000 /**
47001 * @cfg {Function} launch An optional function that will be called when the Application is ready to be
47002 * launched. This is normally used to render any initial UI required by your application
47003 * @accessor
47004 */
47005 launch: Ext.emptyFn,
47006
47007 /**
47008 * @private
47009 * @cfg {Boolean} enableLoader Private config to disable loading of Profiles at application construct time.
47010 * This is used by Sencha's unit test suite to test _Application.js_ in isolation and is likely to be removed
47011 * in favor of a more pleasing solution by the time you use it.
47012 * @accessor
47013 */
47014 enableLoader: true,
47015
47016 /**
47017 * @cfg {String[]} requires An array of extra dependencies, to be required after this application's {@link #name} config
47018 * has been processed properly, but before anything else to ensure overrides get executed first.
47019 * @accessor
47020 */
47021 requires: [],
47022
47023 /**
47024 * @cfg {String} themeVariationPrefix Used only with {@link themeVariation} this prefix will be added before the variation as a class on the HTML
47025 * tag of your application.
47026 */
47027 themeVariationPrefix: Ext.baseCSSPrefix + 'theme-variation-',
47028
47029 /**
47030 * @cfg {String} themeVariationTransitionCls This is only used with {@link themeVariation}. The Class provided will be added to the HTML tag
47031 * then removed once the transition is complete. The duration of this delayed removal is parsed from the class itself, for example if the class
47032 * has the property 'transition: color 4s, background 6s, background-color 1s' the delay will be 6s (the largest time used in that class.
47033 *
47034 * @accessor
47035 */
47036 themeVariationTransitionCls: null,
47037
47038 /**
47039 * @cfg {String/Function} themeVariation A string to determine the variation on the current theme being used. This string will be prefixed by
47040 * {@link themeVariationPrefix} and the resulting string will be added to the HTML tag of your application. If a function is provided that function
47041 * must return a string.
47042 *
47043 * //This will result in 'x-theme-variation-dark' being added as a class to the html tag of your application
47044 * MyApp.app.setThemeVariation("dark");
47045 *
47046 * @accessor
47047 */
47048 themeVariation: null
47049 },
47050
47051 /**
47052 * Constructs a new Application instance.
47053 */
47054 constructor: function(config) {
47055 config = config || {};
47056
47057 Ext.applyIf(config, {
47058 application: this
47059 });
47060
47061 this.initConfig(config);
47062
47063 //it's common to pass in functions to an application but because they are not predictable config names they
47064 //aren't ordinarily placed onto this so we need to do it manually
47065 for (var key in config) {
47066 this[key] = config[key];
47067 }
47068
47069
47070 //<debug>
47071 Ext.Loader.setConfig({ enabled: true });
47072 //</debug>
47073
47074 Ext.require(this.getRequires(), function() {
47075 if (this.getEnableLoader() !== false) {
47076 Ext.require(this.getProfiles(), this.onProfilesLoaded, this);
47077 }
47078 }, this);
47079 },
47080
47081 /**
47082 * Dispatches a given {@link Ext.app.Action} to the relevant Controller instance. This is not usually called
47083 * directly by the developer, instead Sencha Touch's History support picks up on changes to the browser's url
47084 * and calls dispatch automatically.
47085 * @param {Ext.app.Action} action The action to dispatch.
47086 * @param {Boolean} [addToHistory=true] Sets the browser's url to the action's url.
47087 */
47088 dispatch: function(action, addToHistory) {
47089 action = action || {};
47090 Ext.applyIf(action, {
47091 application: this
47092 });
47093
47094 action = Ext.factory(action, Ext.app.Action);
47095
47096 if (action) {
47097 var profile = this.getCurrentProfile(),
47098 profileNS = profile ? profile.getNamespace() : undefined,
47099 controller = this.getController(action.getController(), profileNS);
47100
47101 if (controller) {
47102 if (addToHistory !== false) {
47103 this.getHistory().add(action, true);
47104 }
47105
47106 controller.execute(action);
47107 }
47108 }
47109 },
47110
47111 /**
47112 * Redirects the browser to the given url. This only affects the url after the '#'. You can pass in either a String
47113 * or a Model instance - if a Model instance is defined its {@link Ext.data.Model#toUrl toUrl} function is called,
47114 * which returns a string representing the url for that model. Internally, this uses your application's
47115 * {@link Ext.app.Router Router} to decode the url into a matching controller action and then calls
47116 * {@link #dispatch}.
47117 * @param {String/Ext.data.Model} url The String url to redirect to.
47118 */
47119 redirectTo: function(url) {
47120 if (Ext.data && Ext.data.Model && url instanceof Ext.data.Model) {
47121 var record = url;
47122
47123 url = record.toUrl();
47124 }
47125
47126 var decoded = this.getRouter().recognize(url);
47127
47128 if (decoded) {
47129 decoded.url = url;
47130 if (record) {
47131 decoded.data = {};
47132 decoded.data.record = record;
47133 }
47134 return this.dispatch(decoded);
47135 }
47136 },
47137
47138 /**
47139 * @private
47140 * (documented on Controller's control config)
47141 */
47142 control: function(selectors, controller) {
47143 //if the controller is not defined, use this instead (the application instance)
47144 controller = controller || this;
47145
47146 var dispatcher = this.getEventDispatcher(),
47147 refs = (controller) ? controller.getRefs() : {},
47148 selector, eventName, listener, listeners, ref;
47149
47150 for (selector in selectors) {
47151 if (selectors.hasOwnProperty(selector)) {
47152 listeners = selectors[selector];
47153 ref = refs[selector];
47154
47155 //refs can be used in place of selectors
47156 if (ref) {
47157 selector = ref.selector || ref;
47158 }
47159 for (eventName in listeners) {
47160 listener = listeners[eventName];
47161
47162 if (Ext.isString(listener)) {
47163 listener = controller[listener];
47164 }
47165
47166 dispatcher.addListener('component', selector, eventName, listener, controller);
47167 }
47168 }
47169 }
47170 },
47171
47172 /**
47173 * Returns the Controller instance for the given controller name.
47174 * @param {String} name The name of the Controller.
47175 * @param {String} [profileName] Optional profile name. If passed, this is the same as calling
47176 * `getController('profileName.controllerName')`.
47177 * @return {Ext.app.Controller} controller instance or undefined.
47178 */
47179 getController: function(name, profileName) {
47180 var instances = this.getControllerInstances(),
47181 appName = this.getName(),
47182 format = Ext.String.format,
47183 topLevelName;
47184
47185 if (name instanceof Ext.app.Controller) {
47186 return name;
47187 }
47188
47189 if (instances[name]) {
47190 return instances[name];
47191 } else {
47192 topLevelName = format("{0}.controller.{1}", appName, name);
47193 profileName = format("{0}.controller.{1}.{2}", appName, profileName, name);
47194
47195 return instances[profileName] || instances[topLevelName];
47196 }
47197 },
47198
47199 /**
47200 * @private
47201 * Callback that is invoked when all of the configured Profiles have been loaded. Detects the current profile and
47202 * gathers any additional dependencies from that profile, then loads all of those dependencies.
47203 */
47204 onProfilesLoaded: function() {
47205 var profiles = this.getProfiles(),
47206 length = profiles.length,
47207 instances = [],
47208 requires = this.gatherDependencies(),
47209 current, i, profileDeps;
47210
47211 for (i = 0; i < length; i++) {
47212 instances[i] = Ext.create(profiles[i], {
47213 application: this
47214 });
47215
47216 /*
47217 * Note that we actually require all of the dependencies for all Profiles - this is so that we can produce
47218 * a single build file that will work on all defined Profiles. Although the other classes will be loaded,
47219 * the correct Profile will still be identified and the other classes ignored. While this feels somewhat
47220 * inefficient, the majority of the bulk of an application is likely to be the framework itself. The bigger
47221 * the app though, the bigger the effect of this inefficiency so ideally we will create a way to create and
47222 * load Profile-specific builds in a future release.
47223 */
47224 profileDeps = instances[i].getDependencies();
47225 requires = requires.concat(profileDeps.all);
47226
47227 if (instances[i].isActive() && !current) {
47228 current = instances[i];
47229
47230 this.setCurrentProfile(current);
47231
47232 this.setControllers(this.getControllers().concat(profileDeps.controller));
47233 this.setModels(this.getModels().concat(profileDeps.model));
47234 this.setViews(this.getViews().concat(profileDeps.view));
47235 this.setStores(this.getStores().concat(profileDeps.store));
47236 }
47237 }
47238
47239 this.setProfileInstances(instances);
47240 Ext.require(requires, this.loadControllerDependencies, this);
47241 },
47242
47243 /**
47244 * @private
47245 * Controllers can also specify dependencies, so we grab them all here and require them.
47246 */
47247 loadControllerDependencies: function() {
47248 this.instantiateControllers();
47249
47250 var controllers = this.getControllerInstances(),
47251 classes = [],
47252 stores = [],
47253 i, controller, controllerStores, name;
47254
47255 for (name in controllers) {
47256 controller = controllers[name];
47257 controllerStores = controller.getStores();
47258 stores = stores.concat(controllerStores);
47259
47260 classes = classes.concat(controller.getModels().concat(controller.getViews()).concat(controllerStores));
47261 }
47262
47263 this.setStores(this.getStores().concat(stores));
47264
47265 Ext.require(classes, this.onDependenciesLoaded, this);
47266 },
47267
47268 /**
47269 * @private
47270 * Callback that is invoked when all of the Application, Controller and Profile dependencies have been loaded.
47271 * Launches the controllers, then the profile and application.
47272 */
47273 onDependenciesLoaded: function() {
47274 var me = this,
47275 profile = this.getCurrentProfile(),
47276 launcher = this.getLaunch(),
47277 controllers, name;
47278
47279 this.instantiateStores();
47280
47281
47282 controllers = this.getControllerInstances();
47283
47284 for (name in controllers) {
47285 controllers[name].init(this);
47286 }
47287
47288 if (profile) {
47289 profile.launch();
47290 }
47291
47292 launcher.call(me);
47293
47294 for (name in controllers) {
47295 //<debug warn>
47296 if (controllers[name] && !(controllers[name] instanceof Ext.app.Controller)) {
47297 Ext.Logger.warn("The controller '" + name + "' doesn't have a launch method. Are you sure it extends from Ext.app.Controller?");
47298 } else {
47299 //</debug>
47300 controllers[name].launch(this);
47301 //<debug warn>
47302 }
47303 //</debug>
47304 }
47305
47306 me.redirectTo(window.location.hash.substr(1));
47307 },
47308
47309 /**
47310 * @private
47311 * Gathers up all of the previously computed MVCS dependencies into a single array that we can pass to {@link Ext#require}.
47312 */
47313 gatherDependencies: function() {
47314 var classes = this.getModels().concat(this.getViews()).concat(this.getControllers());
47315
47316 Ext.each(this.getStores(), function(storeName) {
47317 if (Ext.isString(storeName)) {
47318 classes.push(storeName);
47319 }
47320 }, this);
47321
47322 return classes;
47323 },
47324
47325 /**
47326 * @private
47327 * Should be called after dependencies are loaded, instantiates all of the Stores specified in the {@link #stores}
47328 * config. For each item in the stores array we make sure the Store is instantiated. When strings are specified,
47329 * the corresponding _app/store/StoreName.js_ was loaded so we now instantiate a `MyApp.store.StoreName`, giving it the
47330 * id `StoreName`.
47331 */
47332 instantiateStores: function() {
47333 var stores = this.getStores(),
47334 length = stores.length,
47335 store, storeClass, storeName, splits, i;
47336
47337 for (i = 0; i < length; i++) {
47338 store = stores[i];
47339
47340 if (Ext.data && Ext.data.Store && !(store instanceof Ext.data.Store)) {
47341 if (Ext.isString(store)) {
47342 storeName = store;
47343 storeClass = Ext.ClassManager.classes[store];
47344
47345 store = {
47346 xclass: store
47347 };
47348
47349 //we don't want to wipe out a configured storeId in the app's Store subclass so need
47350 //to check for this first
47351 if (storeClass.prototype.defaultConfig.storeId === undefined) {
47352 splits = storeName.split('.');
47353 store.id = splits[splits.length - 1];
47354 }
47355 }
47356
47357 stores[i] = Ext.factory(store, Ext.data.Store);
47358 }
47359 }
47360
47361 this.setStores(stores);
47362 },
47363
47364 /**
47365 * @private
47366 * Called once all of our controllers have been loaded
47367 */
47368 instantiateControllers: function() {
47369 var controllerNames = this.getControllers(),
47370 instances = {},
47371 length = controllerNames.length,
47372 name, i;
47373
47374 for (i = 0; i < length; i++) {
47375 name = controllerNames[i];
47376
47377 instances[name] = Ext.create(name, {
47378 application: this
47379 });
47380 }
47381
47382 return this.setControllerInstances(instances);
47383 },
47384
47385 /**
47386 * @private
47387 * As a convenience developers can locally qualify controller names (e.g. 'MyController' vs
47388 * 'MyApp.controller.MyController'). This just makes sure everything ends up fully qualified
47389 */
47390 applyControllers: function(controllers) {
47391 return this.getFullyQualified(controllers, 'controller');
47392 },
47393
47394 /**
47395 * @private
47396 * As a convenience developers can locally qualify profile names (e.g. 'MyProfile' vs
47397 * 'MyApp.profile.MyProfile'). This just makes sure everything ends up fully qualified
47398 */
47399 applyProfiles: function(profiles) {
47400 return this.getFullyQualified(profiles, 'profile');
47401 },
47402
47403 /**
47404 * @private
47405 * Checks that the name configuration has any whitespace, and trims them if found.
47406 */
47407 applyName: function(name) {
47408 var oldName;
47409 if (name && name.match(/ /g)) {
47410 oldName = name;
47411 name = name.replace(/ /g, "");
47412
47413 // <debug>
47414 Ext.Logger.warn('Attempting to create an application with a name which contains whitespace ("' + oldName + '"). Renamed to "' + name + '".');
47415 // </debug>
47416 }
47417
47418 return name;
47419 },
47420
47421 /**
47422 * @private
47423 * Makes sure the app namespace exists, sets the `app` property of the namespace to this application and sets its
47424 * loading path (checks to make sure the path hadn't already been set via Ext.Loader.setPath)
47425 */
47426 updateName: function(newName) {
47427 Ext.ClassManager.setNamespace(newName + '.app', this);
47428
47429 if (!Ext.Loader.config.paths[newName]) {
47430 Ext.Loader.setPath(newName, this.getAppFolder());
47431 }
47432 },
47433
47434 /**
47435 * @private
47436 */
47437 applyRouter: function(config) {
47438 return Ext.factory(config, Ext.app.Router, this.getRouter());
47439 },
47440
47441 /**
47442 * @private
47443 */
47444 applyHistory: function(config) {
47445 var history = Ext.factory(config, Ext.app.History, this.getHistory());
47446
47447 history.on('change', this.onHistoryChange, this);
47448
47449 return history;
47450 },
47451
47452 /**
47453 * @private
47454 */
47455 onHistoryChange: function(url) {
47456 this.dispatch(this.getRouter().recognize(url), false);
47457 },
47458
47459 updateThemeVariation: function(newVariation, oldVariation) {
47460 var html = Ext.getBody().getParent(),
47461 themeVariationPrefix = this.getThemeVariationPrefix() || "",
47462 transitionCls = this.getThemeVariationTransitionCls();
47463
47464 if (Ext.isFunction(newVariation)) {
47465 newVariation = newVariation.call(this);
47466 }
47467
47468 if(!Ext.isString(newVariation)) {
47469 Ext.Error.raise("Theme variation must be a String.'");
47470 }
47471
47472 if(transitionCls) {
47473 var css = "", duration = 0,
47474 rules = document.styleSheets[0].cssRules,
47475 i, rule, times, time;
47476
47477 html.addCls(transitionCls);
47478 for(i in rules) {
47479 rule = rules[i];
47480 if(rule.selectorText && rule.selectorText.indexOf("." + transitionCls) >=1) {
47481 css += rule.cssText;
47482 }
47483 }
47484
47485 times = css.match(/[0-9]+s/g);
47486 for(i in times) {
47487 time = parseInt(times[i]);
47488 if(time > duration) {
47489 duration = time;
47490 }
47491 }
47492
47493 if(this.$themeVariationChangeTimeout) {
47494 clearTimeout(this.$themeVariationChangeTimeout);
47495 this.$themeVariationChangeTimeout = null;
47496 }
47497
47498 this.$themeVariationChangeTimeout = Ext.defer(function() {
47499 html.removeCls(transitionCls);
47500 }, time * 1000);
47501 }
47502
47503 html.removeCls(themeVariationPrefix + oldVariation);
47504 html.addCls(themeVariationPrefix + newVariation);
47505 }
47506 }, function() {
47507 });
47508
47509
47510
47511 /**
47512 * @private
47513 */
47514 Ext.define('Ext.carousel.Item', {
47515 extend: Ext.Decorator ,
47516
47517 config: {
47518 baseCls: 'x-carousel-item',
47519 component: null,
47520 translatable: true
47521 }
47522 });
47523
47524 /**
47525 * A private utility class used by Ext.Carousel to create indicators.
47526 * @private
47527 */
47528 Ext.define('Ext.carousel.Indicator', {
47529 extend: Ext.Component ,
47530 xtype : 'carouselindicator',
47531 alternateClassName: 'Ext.Carousel.Indicator',
47532
47533 config: {
47534 /**
47535 * @cfg
47536 * @inheritdoc
47537 */
47538 baseCls: Ext.baseCSSPrefix + 'carousel-indicator',
47539
47540 direction: 'horizontal'
47541 },
47542
47543 /**
47544 * @event previous
47545 * Fires when this indicator is tapped on the left half
47546 * @param {Ext.carousel.Indicator} this
47547 */
47548
47549 /**
47550 * @event next
47551 * Fires when this indicator is tapped on the right half
47552 * @param {Ext.carousel.Indicator} this
47553 */
47554
47555 initialize: function() {
47556 this.callParent();
47557
47558 this.indicators = [];
47559
47560 this.element.on({
47561 tap: 'onTap',
47562 scope: this
47563 });
47564 },
47565
47566 updateDirection: function(newDirection, oldDirection) {
47567 var baseCls = this.getBaseCls();
47568
47569 this.element.replaceCls(oldDirection, newDirection, baseCls);
47570
47571 if (newDirection === 'horizontal') {
47572 this.setBottom(0);
47573 this.setRight(null);
47574 }
47575 else {
47576 this.setRight(0);
47577 this.setBottom(null);
47578 }
47579 },
47580
47581 addIndicator: function() {
47582 this.indicators.push(this.element.createChild({
47583 tag: 'span'
47584 }));
47585 },
47586
47587 removeIndicator: function() {
47588 var indicators = this.indicators;
47589
47590 if (indicators.length > 0) {
47591 indicators.pop().destroy();
47592 }
47593 },
47594
47595 setActiveIndex: function(index) {
47596 var indicators = this.indicators,
47597 currentActiveIndex = this.activeIndex,
47598 currentActiveItem = indicators[currentActiveIndex],
47599 activeItem = indicators[index],
47600 baseCls = this.getBaseCls();
47601
47602 if (currentActiveItem) {
47603 currentActiveItem.removeCls(baseCls, null, 'active');
47604 }
47605
47606 if (activeItem) {
47607 activeItem.addCls(baseCls, null, 'active');
47608 }
47609
47610 this.activeIndex = index;
47611
47612 return this;
47613 },
47614
47615 // @private
47616 onTap: function(e) {
47617 var touch = e.touch,
47618 box = this.element.getPageBox(),
47619 centerX = box.left + (box.width / 2),
47620 centerY = box.top + (box.height / 2),
47621 direction = this.getDirection();
47622
47623 if ((direction === 'horizontal' && touch.pageX >= centerX) || (direction === 'vertical' && touch.pageY >= centerY)) {
47624 this.fireEvent('next', this);
47625 }
47626 else {
47627 this.fireEvent('previous', this);
47628 }
47629 },
47630
47631 destroy: function() {
47632 var indicators = this.indicators,
47633 i, ln, indicator;
47634
47635 for (i = 0,ln = indicators.length; i < ln; i++) {
47636 indicator = indicators[i];
47637 indicator.destroy();
47638 }
47639
47640 indicators.length = 0;
47641
47642 this.callParent();
47643 }
47644 });
47645
47646 /**
47647 * @private
47648 */
47649 Ext.define('Ext.util.TranslatableGroup', {
47650 extend: Ext.util.translatable.Abstract ,
47651
47652 config: {
47653 items: [],
47654
47655 activeIndex: 0,
47656
47657 itemLength: {
47658 x: 0,
47659 y: 0
47660 }
47661 },
47662
47663 applyItems: function(items) {
47664 return Ext.Array.from(items);
47665 },
47666
47667 doTranslate: function(x, y) {
47668 var items = this.getItems(),
47669 activeIndex = this.getActiveIndex(),
47670 itemLength = this.getItemLength(),
47671 itemLengthX = itemLength.x,
47672 itemLengthY = itemLength.y,
47673 useX = Ext.isNumber(x),
47674 useY = Ext.isNumber(y),
47675 offset, i, ln, item, translateX, translateY;
47676
47677 for (i = 0, ln = items.length; i < ln; i++) {
47678 item = items[i];
47679
47680 if (item) {
47681 offset = (i - activeIndex);
47682
47683 if (useX) {
47684 translateX = x + offset * itemLengthX;
47685 }
47686
47687 if (useY) {
47688 translateY = y + offset * itemLengthY;
47689 }
47690
47691 item.translate(translateX, translateY);
47692 }
47693 }
47694 }
47695 });
47696
47697 /**
47698 * @class Ext.carousel.Carousel
47699 * @author Jacky Nguyen <jacky@sencha.com>
47700 *
47701 * Carousels, like [tabs](../../../components/tabpanel.html), are a great way to allow the user to swipe through multiple full-screen pages.
47702 * A Carousel shows only one of its pages at a time but allows you to swipe through with your finger.
47703 *
47704 * Carousels can be oriented either horizontally or vertically and are easy to configure - they just work like any other
47705 * Container. Here's how to set up a simple horizontal Carousel:
47706 *
47707 * @example
47708 * Ext.create('Ext.Carousel', {
47709 * fullscreen: true,
47710 *
47711 * defaults: {
47712 * styleHtmlContent: true
47713 * },
47714 *
47715 * items: [
47716 * {
47717 * html : 'Item 1',
47718 * style: 'background-color: #5E99CC'
47719 * },
47720 * {
47721 * html : 'Item 2',
47722 * style: 'background-color: #759E60'
47723 * },
47724 * {
47725 * html : 'Item 3'
47726 * }
47727 * ]
47728 * });
47729 *
47730 * We can also make Carousels orient themselves vertically:
47731 *
47732 * @example preview
47733 * Ext.create('Ext.Carousel', {
47734 * fullscreen: true,
47735 * direction: 'vertical',
47736 *
47737 * defaults: {
47738 * styleHtmlContent: true
47739 * },
47740 *
47741 * items: [
47742 * {
47743 * html : 'Item 1',
47744 * style: 'background-color: #759E60'
47745 * },
47746 * {
47747 * html : 'Item 2',
47748 * style: 'background-color: #5E99CC'
47749 * }
47750 * ]
47751 * });
47752 *
47753 * ### Common Configurations
47754 * * {@link #ui} defines the style of the carousel
47755 * * {@link #direction} defines the direction of the carousel
47756 * * {@link #indicator} defines if the indicator show be shown
47757 *
47758 * ### Useful Methods
47759 * * {@link #next} moves to the next card
47760 * * {@link #previous} moves to the previous card
47761 * * {@link #setActiveItem} moves to the passed card
47762 *
47763 * ## Further Reading
47764 *
47765 * For more information about Carousels see the [Carousel guide](../../../components/carousel.html).
47766 */
47767 Ext.define('Ext.carousel.Carousel', {
47768 extend: Ext.Container ,
47769
47770 alternateClassName: 'Ext.Carousel',
47771
47772 xtype: 'carousel',
47773
47774
47775
47776
47777
47778
47779
47780
47781 config: {
47782 /**
47783 * @cfg layout
47784 * Hide layout config in Carousel. It only causes confusion.
47785 * @accessor
47786 * @private
47787 */
47788
47789 /**
47790 * @cfg
47791 * @inheritdoc
47792 */
47793 baseCls: 'x-carousel',
47794
47795 /**
47796 * @cfg {String} direction
47797 * The direction of the Carousel, either 'horizontal' or 'vertical'.
47798 * @accessor
47799 */
47800 direction: 'horizontal',
47801
47802 directionLock: false,
47803
47804 animation: {
47805 duration: 250,
47806 easing: {
47807 type: 'ease-out'
47808 }
47809 },
47810
47811 /**
47812 * @cfg draggable
47813 * @hide
47814 */
47815
47816 /**
47817 * @cfg {Boolean} indicator
47818 * Provides an indicator while toggling between child items to let the user
47819 * know where they are in the card stack.
47820 * @accessor
47821 */
47822 indicator: true,
47823
47824 /**
47825 * @cfg {String} ui
47826 * Style options for Carousel. Default is 'dark'. 'light' is also available.
47827 * @accessor
47828 */
47829 ui: 'dark',
47830
47831 itemConfig: {},
47832
47833 bufferSize: 1,
47834
47835 itemLength: null
47836 },
47837
47838 itemLength: 0,
47839
47840 offset: 0,
47841
47842 flickStartOffset: 0,
47843
47844 flickStartTime: 0,
47845
47846 dragDirection: 0,
47847
47848 count: 0,
47849
47850 painted: false,
47851
47852 activeIndex: -1,
47853
47854 beforeInitialize: function() {
47855 this.element.on({
47856 dragstart: 'onDragStart',
47857 drag: 'onDrag',
47858 dragend: 'onDragEnd',
47859 scope: this
47860 });
47861
47862 this.element.on('resize', 'onSizeChange', this);
47863
47864 this.carouselItems = [];
47865
47866 this.orderedCarouselItems = [];
47867
47868 this.inactiveCarouselItems = [];
47869
47870 this.hiddenTranslation = 0;
47871 },
47872
47873 updateBufferSize: function(size) {
47874 var ItemClass = Ext.carousel.Item,
47875 total = size * 2 + 1,
47876 isRendered = this.isRendered(),
47877 innerElement = this.innerElement,
47878 items = this.carouselItems,
47879 ln = items.length,
47880 itemConfig = this.getItemConfig(),
47881 itemLength = this.getItemLength(),
47882 direction = this.getDirection(),
47883 setterName = direction === 'horizontal' ? 'setWidth' : 'setHeight',
47884 i, item;
47885
47886 for (i = ln; i < total; i++) {
47887 item = Ext.factory(itemConfig, ItemClass);
47888
47889 if (itemLength) {
47890 item[setterName].call(item, itemLength);
47891 }
47892 item.setLayoutSizeFlags(this.LAYOUT_BOTH);
47893 items.push(item);
47894 innerElement.append(item.renderElement);
47895
47896 if (isRendered && item.setRendered(true)) {
47897 item.fireEvent('renderedchange', this, item, true);
47898 }
47899 }
47900
47901 this.getTranslatable().setActiveIndex(size);
47902 },
47903
47904 setRendered: function(rendered) {
47905 var wasRendered = this.rendered;
47906
47907 if (rendered !== wasRendered) {
47908 this.rendered = rendered;
47909
47910 var items = this.items.items,
47911 carouselItems = this.carouselItems,
47912 i, ln, item;
47913
47914 for (i = 0,ln = items.length; i < ln; i++) {
47915 item = items[i];
47916
47917 if (!item.isInnerItem()) {
47918 item.setRendered(rendered);
47919 }
47920 }
47921
47922 for (i = 0,ln = carouselItems.length; i < ln; i++) {
47923 carouselItems[i].setRendered(rendered);
47924 }
47925
47926 return true;
47927 }
47928
47929 return false;
47930 },
47931
47932 onSizeChange: function() {
47933 this.refreshSizing();
47934 this.refreshCarouselItems();
47935 this.refreshActiveItem();
47936 },
47937
47938 onItemAdd: function(item, index) {
47939 this.callParent(arguments);
47940
47941 var innerIndex = this.getInnerItems().indexOf(item),
47942 indicator = this.getIndicator();
47943
47944 if (indicator && item.isInnerItem()) {
47945 indicator.addIndicator();
47946 }
47947
47948 if (innerIndex <= this.getActiveIndex()) {
47949 this.refreshActiveIndex();
47950 }
47951
47952 if (this.isIndexDirty(innerIndex) && !this.isItemsInitializing) {
47953 this.refreshActiveItem();
47954 }
47955 },
47956
47957 doItemLayoutAdd: function(item) {
47958 if (item.isInnerItem()) {
47959 return;
47960 }
47961
47962 this.callParent(arguments);
47963 },
47964
47965 onItemRemove: function(item, index) {
47966 this.callParent(arguments);
47967
47968 var innerIndex = this.getInnerItems().indexOf(item),
47969 indicator = this.getIndicator(),
47970 carouselItems = this.carouselItems,
47971 i, ln, carouselItem;
47972
47973 if (item.isInnerItem() && indicator) {
47974 indicator.removeIndicator();
47975 }
47976
47977 if (innerIndex <= this.getActiveIndex()) {
47978 this.refreshActiveIndex();
47979 }
47980
47981 if (this.isIndexDirty(innerIndex)) {
47982 for (i = 0,ln = carouselItems.length; i < ln; i++) {
47983 carouselItem = carouselItems[i];
47984
47985 if (carouselItem.getComponent() === item) {
47986 carouselItem.setComponent(null);
47987 }
47988 }
47989
47990 this.refreshActiveItem();
47991 }
47992 },
47993
47994 doItemLayoutRemove: function(item) {
47995 if (item.isInnerItem()) {
47996 return;
47997 }
47998
47999 this.callParent(arguments);
48000 },
48001
48002 onInnerItemMove: function(item, toIndex, fromIndex) {
48003 if ((this.isIndexDirty(toIndex) || this.isIndexDirty(fromIndex))) {
48004 this.refreshActiveItem();
48005 }
48006 },
48007
48008 doItemLayoutMove: function(item) {
48009 if (item.isInnerItem()) {
48010 return;
48011 }
48012
48013 this.callParent(arguments);
48014 },
48015
48016 isIndexDirty: function(index) {
48017 var activeIndex = this.getActiveIndex(),
48018 bufferSize = this.getBufferSize();
48019
48020 return (index >= activeIndex - bufferSize && index <= activeIndex + bufferSize);
48021 },
48022
48023 getTranslatable: function() {
48024 var translatable = this.translatable;
48025
48026 if (!translatable) {
48027 this.translatable = translatable = new Ext.util.TranslatableGroup;
48028 translatable.setItems(this.orderedCarouselItems);
48029 translatable.on('animationend', 'onAnimationEnd', this);
48030 }
48031
48032 return translatable;
48033 },
48034
48035 onDragStart: function(e) {
48036 var direction = this.getDirection(),
48037 absDeltaX = e.absDeltaX,
48038 absDeltaY = e.absDeltaY,
48039 directionLock = this.getDirectionLock();
48040
48041 this.isDragging = true;
48042
48043 if (directionLock) {
48044 if ((direction === 'horizontal' && absDeltaX > absDeltaY)
48045 || (direction === 'vertical' && absDeltaY > absDeltaX)) {
48046 e.stopPropagation();
48047 }
48048 else {
48049 this.isDragging = false;
48050 return;
48051 }
48052 }
48053
48054 this.getTranslatable().stopAnimation();
48055
48056 this.dragStartOffset = this.offset;
48057 this.dragDirection = 0;
48058 },
48059
48060 onDrag: function(e) {
48061 if (!this.isDragging) {
48062 return;
48063 }
48064
48065 var startOffset = this.dragStartOffset,
48066 direction = this.getDirection(),
48067 delta = direction === 'horizontal' ? e.deltaX : e.deltaY,
48068 lastOffset = this.offset,
48069 flickStartTime = this.flickStartTime,
48070 dragDirection = this.dragDirection,
48071 now = Ext.Date.now(),
48072 currentActiveIndex = this.getActiveIndex(),
48073 maxIndex = this.getMaxItemIndex(),
48074 lastDragDirection = dragDirection,
48075 offset;
48076
48077 if ((currentActiveIndex === 0 && delta > 0) || (currentActiveIndex === maxIndex && delta < 0)) {
48078 delta *= 0.5;
48079 }
48080
48081 offset = startOffset + delta;
48082
48083 if (offset > lastOffset) {
48084 dragDirection = 1;
48085 }
48086 else if (offset < lastOffset) {
48087 dragDirection = -1;
48088 }
48089
48090 if (dragDirection !== lastDragDirection || (now - flickStartTime) > 300) {
48091 this.flickStartOffset = lastOffset;
48092 this.flickStartTime = now;
48093 }
48094
48095 this.dragDirection = dragDirection;
48096
48097 this.setOffset(offset);
48098 },
48099
48100 onDragEnd: function(e) {
48101 if (!this.isDragging) {
48102 return;
48103 }
48104
48105 this.onDrag(e);
48106
48107 this.isDragging = false;
48108
48109 var now = Ext.Date.now(),
48110 itemLength = this.itemLength,
48111 threshold = itemLength / 2,
48112 offset = this.offset,
48113 activeIndex = this.getActiveIndex(),
48114 maxIndex = this.getMaxItemIndex(),
48115 animationDirection = 0,
48116 flickDistance = offset - this.flickStartOffset,
48117 flickDuration = now - this.flickStartTime,
48118 indicator = this.getIndicator(),
48119 velocity;
48120
48121 if (flickDuration > 0 && Math.abs(flickDistance) >= 10) {
48122 velocity = flickDistance / flickDuration;
48123
48124 if (Math.abs(velocity) >= 1) {
48125 if (velocity < 0 && activeIndex < maxIndex) {
48126 animationDirection = -1;
48127 }
48128 else if (velocity > 0 && activeIndex > 0) {
48129 animationDirection = 1;
48130 }
48131 }
48132 }
48133
48134 if (animationDirection === 0) {
48135 if (activeIndex < maxIndex && offset < -threshold) {
48136 animationDirection = -1;
48137 }
48138 else if (activeIndex > 0 && offset > threshold) {
48139 animationDirection = 1;
48140 }
48141 }
48142
48143 if (indicator) {
48144 indicator.setActiveIndex(activeIndex - animationDirection);
48145 }
48146
48147 this.animationDirection = animationDirection;
48148
48149 this.setOffsetAnimated(animationDirection * itemLength);
48150 },
48151
48152 applyAnimation: function(animation) {
48153 animation.easing = Ext.factory(animation.easing, Ext.fx.easing.EaseOut);
48154
48155 return animation;
48156 },
48157
48158 updateDirection: function(direction) {
48159 var indicator = this.getIndicator();
48160
48161 this.currentAxis = (direction === 'horizontal') ? 'x' : 'y';
48162
48163 if (indicator) {
48164 indicator.setDirection(direction);
48165 }
48166 },
48167
48168 /**
48169 * @private
48170 * @chainable
48171 */
48172 setOffset: function(offset) {
48173 this.offset = offset;
48174
48175 if (Ext.isNumber(this.itemOffset)) {
48176 this.getTranslatable().translateAxis(this.currentAxis, offset + this.itemOffset);
48177 }
48178
48179 return this;
48180 },
48181
48182 /**
48183 * @private
48184 * @return {Ext.carousel.Carousel} this
48185 * @chainable
48186 */
48187 setOffsetAnimated: function(offset) {
48188 var indicator = this.getIndicator();
48189
48190 if (indicator) {
48191 indicator.setActiveIndex(this.getActiveIndex() - this.animationDirection);
48192 }
48193
48194 this.offset = offset;
48195
48196 this.getTranslatable().translateAxis(this.currentAxis, offset + this.itemOffset, this.getAnimation());
48197
48198 return this;
48199 },
48200
48201 onAnimationEnd: function(translatable) {
48202 var currentActiveIndex = this.getActiveIndex(),
48203 animationDirection = this.animationDirection,
48204 axis = this.currentAxis,
48205 currentOffset = translatable[axis],
48206 itemLength = this.itemLength,
48207 offset;
48208
48209 if (animationDirection === -1) {
48210 offset = itemLength + currentOffset;
48211 }
48212 else if (animationDirection === 1) {
48213 offset = currentOffset - itemLength;
48214 }
48215 else {
48216 offset = currentOffset;
48217 }
48218
48219 offset -= this.itemOffset;
48220 this.offset = offset;
48221 this.setActiveItem(currentActiveIndex - animationDirection);
48222 },
48223
48224 refresh: function() {
48225 this.refreshSizing();
48226 this.refreshActiveItem();
48227 },
48228
48229 refreshSizing: function() {
48230 var element = this.element,
48231 itemLength = this.getItemLength(),
48232 translatableItemLength = {
48233 x: 0,
48234 y: 0
48235 },
48236 itemOffset, containerSize;
48237
48238 if (this.getDirection() === 'horizontal') {
48239 containerSize = element.getWidth();
48240 }
48241 else {
48242 containerSize = element.getHeight();
48243 }
48244
48245 this.hiddenTranslation = -containerSize;
48246
48247 if (itemLength === null) {
48248 itemLength = containerSize;
48249 itemOffset = 0;
48250 }
48251 else {
48252 itemOffset = (containerSize - itemLength) / 2;
48253 }
48254
48255 this.itemLength = itemLength;
48256 this.itemOffset = itemOffset;
48257 translatableItemLength[this.currentAxis] = itemLength;
48258 this.getTranslatable().setItemLength(translatableItemLength);
48259 },
48260
48261 refreshOffset: function() {
48262 this.setOffset(this.offset);
48263 },
48264
48265 refreshActiveItem: function() {
48266 this.doSetActiveItem(this.getActiveItem());
48267 },
48268
48269 /**
48270 * Returns the index of the currently active card.
48271 * @return {Number} The index of the currently active card.
48272 */
48273 getActiveIndex: function() {
48274 return this.activeIndex;
48275 },
48276
48277 refreshActiveIndex: function() {
48278 this.activeIndex = this.getInnerItemIndex(this.getActiveItem());
48279 },
48280
48281 refreshCarouselItems: function() {
48282 var items = this.carouselItems,
48283 i, ln, item;
48284
48285 for (i = 0,ln = items.length; i < ln; i++) {
48286 item = items[i];
48287 item.getTranslatable().refresh();
48288 }
48289
48290 this.refreshInactiveCarouselItems();
48291 },
48292
48293 refreshInactiveCarouselItems: function() {
48294 var items = this.inactiveCarouselItems,
48295 hiddenTranslation = this.hiddenTranslation,
48296 axis = this.currentAxis,
48297 i, ln, item;
48298
48299 for (i = 0,ln = items.length; i < ln; i++) {
48300 item = items[i];
48301 item.translateAxis(axis, hiddenTranslation);
48302 }
48303 },
48304
48305 /**
48306 * @private
48307 * @return {Number}
48308 */
48309 getMaxItemIndex: function() {
48310 return this.innerItems.length - 1;
48311 },
48312
48313 /**
48314 * @private
48315 * @return {Number}
48316 */
48317 getInnerItemIndex: function(item) {
48318 return this.innerItems.indexOf(item);
48319 },
48320
48321 /**
48322 * @private
48323 * @return {Object}
48324 */
48325 getInnerItemAt: function(index) {
48326 return this.innerItems[index];
48327 },
48328
48329 /**
48330 * @private
48331 * @return {Object}
48332 */
48333 applyActiveItem: function() {
48334 var activeItem = this.callParent(arguments),
48335 activeIndex;
48336
48337 if (activeItem) {
48338 activeIndex = this.getInnerItemIndex(activeItem);
48339
48340 if (activeIndex !== -1) {
48341 this.activeIndex = activeIndex;
48342 return activeItem;
48343 }
48344 }
48345 },
48346
48347 doSetActiveItem: function(activeItem) {
48348 var activeIndex = this.getActiveIndex(),
48349 maxIndex = this.getMaxItemIndex(),
48350 indicator = this.getIndicator(),
48351 bufferSize = this.getBufferSize(),
48352 carouselItems = this.carouselItems.slice(),
48353 orderedCarouselItems = this.orderedCarouselItems,
48354 visibleIndexes = {},
48355 visibleItems = {},
48356 visibleItem, component, id, i, index, ln, carouselItem;
48357
48358 if (carouselItems.length === 0) {
48359 return;
48360 }
48361
48362 this.callParent(arguments);
48363
48364 orderedCarouselItems.length = 0;
48365
48366 if (activeItem) {
48367 id = activeItem.getId();
48368 visibleItems[id] = activeItem;
48369 visibleIndexes[id] = bufferSize;
48370
48371 if (activeIndex > 0) {
48372 for (i = 1; i <= bufferSize; i++) {
48373 index = activeIndex - i;
48374 if (index >= 0) {
48375 visibleItem = this.getInnerItemAt(index);
48376 id = visibleItem.getId();
48377 visibleItems[id] = visibleItem;
48378 visibleIndexes[id] = bufferSize - i;
48379 }
48380 else {
48381 break;
48382 }
48383 }
48384 }
48385
48386 if (activeIndex < maxIndex) {
48387 for (i = 1; i <= bufferSize; i++) {
48388 index = activeIndex + i;
48389 if (index <= maxIndex) {
48390 visibleItem = this.getInnerItemAt(index);
48391 id = visibleItem.getId();
48392 visibleItems[id] = visibleItem;
48393 visibleIndexes[id] = bufferSize + i;
48394 }
48395 else {
48396 break;
48397 }
48398 }
48399 }
48400
48401 for (i = 0,ln = carouselItems.length; i < ln; i++) {
48402 carouselItem = carouselItems[i];
48403 component = carouselItem.getComponent();
48404
48405 if (component) {
48406 id = component.getId();
48407
48408 if (visibleIndexes.hasOwnProperty(id)) {
48409 carouselItems.splice(i, 1);
48410 i--;
48411 ln--;
48412 delete visibleItems[id];
48413 orderedCarouselItems[visibleIndexes[id]] = carouselItem;
48414 }
48415 }
48416 }
48417
48418 for (id in visibleItems) {
48419 if (visibleItems.hasOwnProperty(id)) {
48420 visibleItem = visibleItems[id];
48421 carouselItem = carouselItems.pop();
48422 carouselItem.setComponent(visibleItem);
48423 orderedCarouselItems[visibleIndexes[id]] = carouselItem;
48424 }
48425 }
48426 }
48427
48428 this.inactiveCarouselItems.length = 0;
48429 this.inactiveCarouselItems = carouselItems;
48430 this.refreshOffset();
48431 this.refreshInactiveCarouselItems();
48432
48433 if (indicator) {
48434 indicator.setActiveIndex(activeIndex);
48435 }
48436 },
48437
48438 /**
48439 * Switches to the next card.
48440 * @return {Ext.carousel.Carousel} this
48441 * @chainable
48442 */
48443 next: function() {
48444 this.setOffset(0);
48445
48446 if (this.activeIndex === this.getMaxItemIndex()) {
48447 return this;
48448 }
48449
48450 this.animationDirection = -1;
48451 this.setOffsetAnimated(-this.itemLength);
48452 return this;
48453 },
48454
48455 /**
48456 * Switches to the previous card.
48457 * @return {Ext.carousel.Carousel} this
48458 * @chainable
48459 */
48460 previous: function() {
48461 this.setOffset(0);
48462
48463 if (this.activeIndex === 0) {
48464 return this;
48465 }
48466
48467 this.animationDirection = 1;
48468 this.setOffsetAnimated(this.itemLength);
48469 return this;
48470 },
48471
48472 // @private
48473 applyIndicator: function(indicator, currentIndicator) {
48474 return Ext.factory(indicator, Ext.carousel.Indicator, currentIndicator);
48475 },
48476
48477 // @private
48478 updateIndicator: function(indicator) {
48479 if (indicator) {
48480 this.insertFirst(indicator);
48481
48482 indicator.setUi(this.getUi());
48483 indicator.on({
48484 next: 'next',
48485 previous: 'previous',
48486 scope: this
48487 });
48488 }
48489 },
48490
48491 destroy: function() {
48492 var carouselItems = this.carouselItems.slice();
48493
48494 this.carouselItems.length = 0;
48495
48496 Ext.destroy(carouselItems, this.getIndicator(), this.translatable);
48497
48498 this.callParent();
48499 delete this.carouselItems;
48500 }
48501
48502 }, function() {
48503 });
48504
48505 /**
48506 * @class Ext.carousel.Infinite
48507 * @author Jacky Nguyen <jacky@sencha.com>
48508 * @private
48509 *
48510 * The true infinite implementation of Carousel, private for now until it's stable to be public
48511 */
48512 Ext.define('Ext.carousel.Infinite', {
48513 extend: Ext.carousel.Carousel ,
48514
48515 config: {
48516 indicator: null,
48517
48518 maxItemIndex: Infinity,
48519
48520 innerItemConfig: {}
48521 },
48522
48523 applyIndicator: function(indicator) {
48524 //<debug error>
48525 if (indicator) {
48526 Ext.Logger.error("'indicator' in Infinite Carousel implementation is not currently supported", this);
48527 }
48528 //</debug>
48529 return;
48530 },
48531
48532 updateBufferSize: function(size) {
48533 this.callParent(arguments);
48534
48535 var total = size * 2 + 1,
48536 ln = this.innerItems.length,
48537 innerItemConfig = this.getInnerItemConfig(),
48538 i;
48539
48540 this.isItemsInitializing = true;
48541
48542 for (i = ln; i < total; i++) {
48543 this.doAdd(this.factoryItem(innerItemConfig));
48544 }
48545
48546 this.isItemsInitializing = false;
48547
48548 this.rebuildInnerIndexes();
48549 this.refreshActiveItem();
48550 },
48551
48552 updateMaxItemIndex: function(maxIndex, oldMaxIndex) {
48553 if (oldMaxIndex !== undefined) {
48554 var activeIndex = this.getActiveIndex();
48555
48556 if (activeIndex > maxIndex) {
48557 this.setActiveItem(maxIndex);
48558 }
48559 else {
48560 this.rebuildInnerIndexes(activeIndex);
48561 this.refreshActiveItem();
48562 }
48563
48564 }
48565 },
48566
48567 rebuildInnerIndexes: function(activeIndex) {
48568 var indexToItem = this.innerIndexToItem,
48569 idToIndex = this.innerIdToIndex,
48570 items = this.innerItems.slice(),
48571 ln = items.length,
48572 bufferSize = this.getBufferSize(),
48573 maxIndex = this.getMaxItemIndex(),
48574 changedIndexes = [],
48575 i, oldIndex, index, id, item;
48576
48577
48578 if (activeIndex === undefined) {
48579 this.innerIndexToItem = indexToItem = {};
48580 this.innerIdToIndex = idToIndex = {};
48581
48582 for (i = 0; i < ln; i++) {
48583 item = items[i];
48584 id = item.getId();
48585 idToIndex[id] = i;
48586 indexToItem[i] = item;
48587 this.fireEvent('itemindexchange', this, item, i, -1);
48588 }
48589 }
48590 else {
48591 for (i = activeIndex - bufferSize; i <= activeIndex + bufferSize; i++) {
48592 if (i >= 0 && i <= maxIndex) {
48593 if (indexToItem.hasOwnProperty(i)) {
48594 Ext.Array.remove(items, indexToItem[i]);
48595 continue;
48596 }
48597 changedIndexes.push(i);
48598 }
48599 }
48600
48601 for (i = 0,ln = changedIndexes.length; i < ln; i++) {
48602 item = items[i];
48603 id = item.getId();
48604 index = changedIndexes[i];
48605 oldIndex = idToIndex[id];
48606
48607 delete indexToItem[oldIndex];
48608
48609 idToIndex[id] = index;
48610 indexToItem[index] = item;
48611 this.fireEvent('itemindexchange', this, item, index, oldIndex);
48612 }
48613 }
48614 },
48615
48616 reset: function() {
48617 this.rebuildInnerIndexes();
48618 this.setActiveItem(0);
48619 },
48620
48621 refreshItems: function() {
48622 var items = this.innerItems,
48623 idToIndex = this.innerIdToIndex,
48624 index, item, i, ln;
48625
48626 for (i = 0,ln = items.length; i < ln; i++) {
48627 item = items[i];
48628 index = idToIndex[item.getId()];
48629 this.fireEvent('itemindexchange', this, item, index, -1);
48630 }
48631 },
48632
48633 getInnerItemIndex: function(item) {
48634 var index = this.innerIdToIndex[item.getId()];
48635
48636 return (typeof index == 'number') ? index : -1;
48637 },
48638
48639 getInnerItemAt: function(index) {
48640 return this.innerIndexToItem[index];
48641 },
48642
48643 applyActiveItem: function(activeItem) {
48644 this.getItems();
48645 this.getBufferSize();
48646
48647 var maxIndex = this.getMaxItemIndex(),
48648 currentActiveIndex = this.getActiveIndex();
48649
48650 if (typeof activeItem == 'number') {
48651 activeItem = Math.max(0, Math.min(activeItem, maxIndex));
48652
48653 if (activeItem === currentActiveIndex) {
48654 return;
48655 }
48656
48657 this.activeIndex = activeItem;
48658
48659 this.rebuildInnerIndexes(activeItem);
48660
48661 activeItem = this.getInnerItemAt(activeItem);
48662 }
48663
48664 if (activeItem) {
48665 return this.callParent([activeItem]);
48666 }
48667 }
48668 });
48669
48670 (function () {
48671 if (!Ext.global.Float32Array) {
48672 // Typed Array polyfill
48673 var Float32Array = function (array) {
48674 if (typeof array === 'number') {
48675 this.length = array;
48676 } else if ('length' in array) {
48677 this.length = array.length;
48678 for (var i = 0, len = array.length; i < len; i++) {
48679 this[i] = +array[i];
48680 }
48681 }
48682 };
48683
48684 Float32Array.prototype = [];
48685 Ext.global.Float32Array = Float32Array;
48686 }
48687 })();
48688
48689 /**
48690 * Utility class providing mathematics functionalities through all the draw package.
48691 */
48692 Ext.define('Ext.draw.Draw', {
48693 singleton: true,
48694
48695 radian: Math.PI / 180,
48696 pi2: Math.PI * 2,
48697
48698 /**
48699 * Function that returns its first element.
48700 * @param {Mixed} a
48701 * @return {Mixed}
48702 */
48703 reflectFn: function (a) {
48704 return a;
48705 },
48706
48707 /**
48708 * Converting degrees to radians.
48709 * @param {Number} degrees
48710 * @return {Number}
48711 */
48712 rad: function (degrees) {
48713 return degrees % 360 * Math.PI / 180;
48714 },
48715
48716 /**
48717 * Converting radians to degrees.
48718 * @param {Number} radian
48719 * @return {Number}
48720 */
48721 degrees: function (radian) {
48722 return radian * 180 / Math.PI % 360;
48723 },
48724
48725 /**
48726 *
48727 * @param {Object} bbox1
48728 * @param {Object} bbox2
48729 * @param {Number} [padding]
48730 * @return {Boolean}
48731 */
48732 isBBoxIntersect: function (bbox1, bbox2, padding) {
48733 padding = padding || 0;
48734 return (Math.max(bbox1.x, bbox2.x) - padding > Math.min(bbox1.x + bbox1.width, bbox2.x + bbox2.width)) ||
48735 (Math.max(bbox1.y, bbox2.y) - padding > Math.min(bbox1.y + bbox1.height, bbox2.y + bbox2.height));
48736 },
48737
48738 /**
48739 * Natural cubic spline interpolation.
48740 * This algorithm runs in linear time.
48741 *
48742 * @param {Array} points Array of numbers.
48743 */
48744 spline: function (points) {
48745 var i, j, ln = points.length,
48746 nd, d, y, ny,
48747 r = 0,
48748 zs = new Float32Array(points.length),
48749 result = new Float32Array(points.length * 3 - 2);
48750
48751 zs[0] = 0;
48752 zs[ln - 1] = 0;
48753
48754 for (i = 1; i < ln - 1; i++) {
48755 zs[i] = (points[i + 1] + points[i - 1] - 2 * points[i]) - zs[i - 1];
48756 r = 1 / (4 - r);
48757 zs[i] *= r;
48758 }
48759
48760 for (i = ln - 2; i > 0; i--) {
48761 r = 3.732050807568877 + 48.248711305964385 / (-13.928203230275537 + Math.pow(0.07179676972449123, i));
48762 zs[i] -= zs[i + 1] * r;
48763 }
48764
48765 ny = points[0];
48766 nd = ny - zs[0];
48767 for (i = 0, j = 0; i < ln - 1; j += 3) {
48768 y = ny;
48769 d = nd;
48770 i++;
48771 ny = points[i];
48772 nd = ny - zs[i];
48773 result[j] = y;
48774 result[j + 1] = (nd + 2 * d) / 3;
48775 result[j + 2] = (nd * 2 + d) / 3;
48776 }
48777 result[j] = ny;
48778 return result;
48779 },
48780
48781 /**
48782 * @private
48783 *
48784 * Calculates bezier curve control anchor points for a particular point in a path, with a
48785 * smoothing curve applied. The smoothness of the curve is controlled by the 'value' parameter.
48786 * Note that this algorithm assumes that the line being smoothed is normalized going from left
48787 * to right; it makes special adjustments assuming this orientation.
48788 *
48789 * @param {Number} prevX X coordinate of the previous point in the path
48790 * @param {Number} prevY Y coordinate of the previous point in the path
48791 * @param {Number} curX X coordinate of the current point in the path
48792 * @param {Number} curY Y coordinate of the current point in the path
48793 * @param {Number} nextX X coordinate of the next point in the path
48794 * @param {Number} nextY Y coordinate of the next point in the path
48795 * @param {Number} value A value to control the smoothness of the curve; this is used to
48796 * divide the distance between points, so a value of 2 corresponds to
48797 * half the distance between points (a very smooth line) while higher values
48798 * result in less smooth curves. Defaults to 4.
48799 * @return {Object} Object containing x1, y1, x2, y2 bezier control anchor points; x1 and y1
48800 * are the control point for the curve toward the previous path point, and
48801 * x2 and y2 are the control point for the curve toward the next path point.
48802 */
48803 getAnchors: function (prevX, prevY, curX, curY, nextX, nextY, value) {
48804 value = value || 4;
48805 var PI = Math.PI,
48806 halfPI = PI / 2,
48807 abs = Math.abs,
48808 sin = Math.sin,
48809 cos = Math.cos,
48810 atan = Math.atan,
48811 control1Length, control2Length, control1Angle, control2Angle,
48812 control1X, control1Y, control2X, control2Y, alpha;
48813
48814 // Find the length of each control anchor line, by dividing the horizontal distance
48815 // between points by the value parameter.
48816 control1Length = (curX - prevX) / value;
48817 control2Length = (nextX - curX) / value;
48818
48819 // Determine the angle of each control anchor line. If the middle point is a vertical
48820 // turnaround then we force it to a flat horizontal angle to prevent the curve from
48821 // dipping above or below the middle point. Otherwise we use an angle that points
48822 // toward the previous/next target point.
48823 if ((curY >= prevY && curY >= nextY) || (curY <= prevY && curY <= nextY)) {
48824 control1Angle = control2Angle = halfPI;
48825 } else {
48826 control1Angle = atan((curX - prevX) / abs(curY - prevY));
48827 if (prevY < curY) {
48828 control1Angle = PI - control1Angle;
48829 }
48830 control2Angle = atan((nextX - curX) / abs(curY - nextY));
48831 if (nextY < curY) {
48832 control2Angle = PI - control2Angle;
48833 }
48834 }
48835
48836 // Adjust the calculated angles so they point away from each other on the same line
48837 alpha = halfPI - ((control1Angle + control2Angle) % (PI * 2)) / 2;
48838 if (alpha > halfPI) {
48839 alpha -= PI;
48840 }
48841 control1Angle += alpha;
48842 control2Angle += alpha;
48843
48844 // Find the control anchor points from the angles and length
48845 control1X = curX - control1Length * sin(control1Angle);
48846 control1Y = curY + control1Length * cos(control1Angle);
48847 control2X = curX + control2Length * sin(control2Angle);
48848 control2Y = curY + control2Length * cos(control2Angle);
48849
48850 // One last adjustment, make sure that no control anchor point extends vertically past
48851 // its target prev/next point, as that results in curves dipping above or below and
48852 // bending back strangely. If we find this happening we keep the control angle but
48853 // reduce the length of the control line so it stays within bounds.
48854 if ((curY > prevY && control1Y < prevY) || (curY < prevY && control1Y > prevY)) {
48855 control1X += abs(prevY - control1Y) * (control1X - curX) / (control1Y - curY);
48856 control1Y = prevY;
48857 }
48858 if ((curY > nextY && control2Y < nextY) || (curY < nextY && control2Y > nextY)) {
48859 control2X -= abs(nextY - control2Y) * (control2X - curX) / (control2Y - curY);
48860 control2Y = nextY;
48861 }
48862
48863 return {
48864 x1: control1X,
48865 y1: control1Y,
48866 x2: control2X,
48867 y2: control2Y
48868 };
48869 },
48870
48871 /**
48872 * Given coordinates of the points, calculates coordinates of a Bezier curve that goes through them.
48873 * @param dataX x-coordinates of the points.
48874 * @param dataY y-coordinates of the points.
48875 * @param value A value to control the smoothness of the curve.
48876 * @return {Object} Object holding two arrays, for x and y coordinates of the curve.
48877 */
48878 smooth: function (dataX, dataY, value) {
48879 var ln = dataX.length,
48880 prevX, prevY,
48881 curX, curY,
48882 nextX, nextY,
48883 x, y,
48884 smoothX = [], smoothY = [],
48885 i, anchors;
48886
48887 for (i = 0; i < ln - 1; i++) {
48888 prevX = dataX[i];
48889 prevY = dataY[i];
48890 if (i === 0) {
48891 x = prevX;
48892 y = prevY;
48893 smoothX.push(x);
48894 smoothY.push(y);
48895 if (ln === 1) {
48896 break;
48897 }
48898 }
48899 curX = dataX[i+1];
48900 curY = dataY[i+1];
48901 nextX = dataX[i+2];
48902 nextY = dataY[i+2];
48903 if (isNaN(nextX) || isNaN(nextY)) {
48904 smoothX.push(x, curX, curX);
48905 smoothY.push(y, curY, curY);
48906 break;
48907 }
48908 anchors = this.getAnchors(prevX, prevY, curX, curY, nextX, nextY, value);
48909 smoothX.push(x, anchors.x1, curX);
48910 smoothY.push(y, anchors.y1, curY);
48911 x = anchors.x2;
48912 y = anchors.y2;
48913 }
48914 return {
48915 smoothX: smoothX,
48916 smoothY: smoothY
48917 }
48918 },
48919
48920 /**
48921 * @method
48922 * @private
48923 * Work around for iOS.
48924 * Nested 3d-transforms seems to prevent the redraw inside it until some event is fired.
48925 */
48926 updateIOS: Ext.os.is.iOS ? function () {
48927 Ext.getBody().createChild({id: 'frame-workaround', style: 'position: absolute; top: 0px; bottom: 0px; left: 0px; right: 0px; background: rgba(0,0,0,0.001); z-index: 100000'});
48928 Ext.draw.Animator.schedule(function () {Ext.get('frame-workaround').destroy();});
48929 } : Ext.emptyFn
48930 });
48931
48932 /**
48933 * @class Ext.draw.gradient.Gradient
48934 *
48935 * Creates a gradient.
48936 */
48937 Ext.define('Ext.draw.gradient.Gradient', {
48938
48939 isGradient: true,
48940
48941 config: {
48942 /**
48943 * @cfg {Array/Object} Defines the stops of the gradient.
48944 */
48945 stops: []
48946 },
48947
48948 applyStops: function (newStops) {
48949 var stops = [],
48950 ln = newStops.length,
48951 i, stop, color;
48952
48953 for (i = 0; i < ln; i++) {
48954 stop = newStops[i];
48955 color = Ext.draw.Color.fly(stop.color || 'none');
48956 stops.push({
48957 offset: Math.min(1, Math.max(0, 'offset' in stop ? stop.offset : stop.position || 0)),
48958 color: color.toString()
48959 });
48960 }
48961 stops.sort(function (a, b) {
48962 return a.offset - b.offset;
48963 });
48964 return stops;
48965 },
48966
48967 onClassExtended: function (subClass, member) {
48968 if (!member.alias && member.type) {
48969 member.alias = 'gradient.' + member.type;
48970 }
48971 },
48972
48973 constructor: function (config) {
48974 this.initConfig(config);
48975 },
48976
48977 /**
48978 * @protected
48979 * Generates the gradient for the given context.
48980 * @param {Ext.draw.engine.SvgContext} ctx The context.
48981 * @param {Object} bbox
48982 * @return {Object}
48983 */
48984 generateGradient: Ext.emptyFn
48985
48986 });
48987
48988 (function () {
48989 /**
48990 * Represents an RGB color and provides helper functions on it e.g. to get
48991 * color components in HSL color space.
48992 */
48993 Ext.define('Ext.draw.Color', {
48994 statics: {
48995 colorToHexRe: /(.*?)rgb\((\d+),\s*(\d+),\s*(\d+)\)/,
48996 rgbToHexRe: /\s*rgb\((\d+),\s*(\d+),\s*(\d+)\)/,
48997 rgbaToHexRe: /\s*rgba\((\d+),\s*(\d+),\s*(\d+),\s*([\.\d]+)\)/,
48998 hexRe: /\s*#([0-9a-fA-F][0-9a-fA-F]?)([0-9a-fA-F][0-9a-fA-F]?)([0-9a-fA-F][0-9a-fA-F]?)\s*/
48999 },
49000
49001 isColor: true,
49002 /**
49003 * @cfg {Number} lightnessFactor
49004 *
49005 * The default factor to compute the lighter or darker color.
49006 */
49007 lightnessFactor: 0.2,
49008
49009 /**
49010 * @constructor
49011 * @param {Number} red Red component (0..255)
49012 * @param {Number} green Green component (0..255)
49013 * @param {Number} blue Blue component (0..255)
49014 * @param {Number} [alpha=1] (optional) Alpha component (0..1)
49015 */
49016 constructor: function (red, green, blue, alpha) {
49017 this.setRGB(red, green, blue, alpha);
49018 },
49019
49020 setRGB: function (red, green, blue, alpha) {
49021 var me = this;
49022 me.r = Math.min(255, Math.max(0, red));
49023 me.g = Math.min(255, Math.max(0, green));
49024 me.b = Math.min(255, Math.max(0, blue));
49025 if (alpha === undefined) {
49026 me.a = 1;
49027 } else {
49028 me.a = Math.min(1, Math.max(0, alpha));
49029 }
49030 },
49031
49032 /**
49033 * Returns the gray value (0 to 255) of the color.
49034 *
49035 * The gray value is calculated using the formula r*0.3 + g*0.59 + b*0.11.
49036 *
49037 * @return {Number}
49038 */
49039 getGrayscale: function () {
49040 // http://en.wikipedia.org/wiki/Grayscale#Converting_color_to_grayscale
49041 return this.r * 0.3 + this.g * 0.59 + this.b * 0.11;
49042 },
49043
49044 /**
49045 * Get the equivalent HSL components of the color.
49046 * @param {Array} [target] Optional array to receive the values.
49047 * @return {Array}
49048 */
49049 getHSL: function (target) {
49050 var me = this,
49051 r = me.r / 255,
49052 g = me.g / 255,
49053 b = me.b / 255,
49054 max = Math.max(r, g, b),
49055 min = Math.min(r, g, b),
49056 delta = max - min,
49057 h,
49058 s = 0,
49059 l = 0.5 * (max + min);
49060
49061 // min==max means achromatic (hue is undefined)
49062 if (min !== max) {
49063 s = (l < 0.5) ? delta / (max + min) : delta / (2 - max - min);
49064 if (r === max) {
49065 h = 60 * (g - b) / delta;
49066 } else if (g === max) {
49067 h = 120 + 60 * (b - r) / delta;
49068 } else {
49069 h = 240 + 60 * (r - g) / delta;
49070 }
49071 if (h < 0) {
49072 h += 360;
49073 }
49074 if (h >= 360) {
49075 h -= 360;
49076 }
49077 }
49078 if (target) {
49079 target[0] = h;
49080 target[1] = s;
49081 target[2] = l;
49082 } else {
49083 target = [h, s, l];
49084 }
49085 return target;
49086 },
49087
49088 /**
49089 * Set current color based on the specified HSL values.
49090 *
49091 * @param {Number} h Hue component (0..359)
49092 * @param {Number} s Saturation component (0..1)
49093 * @param {Number} l Lightness component (0..1)
49094 * @return this
49095 */
49096 setHSL: function (h, s, l) {
49097 var c, x, m,
49098 abs = Math.abs,
49099 floor = Math.floor;
49100 h = (h % 360 + 360 ) % 360;
49101 s = s > 1 ? 1 : s < 0 ? 0 : s;
49102 l = l > 1 ? 1 : l < 0 ? 0 : l;
49103 if (s === 0 || h === null) {
49104 l *= 255;
49105 this.setRGB(l, l, l);
49106 }
49107 else {
49108 // http://en.wikipedia.org/wiki/HSL_and_HSV#From_HSL
49109 // C is the chroma
49110 // X is the second largest component
49111 // m is the lightness adjustment
49112 h /= 60;
49113 c = s * (1 - abs(2 * l - 1));
49114 x = c * (1 - abs(h - 2 * floor(h / 2) - 1));
49115 m = l - c / 2;
49116 m *= 255;
49117 c *= 255;
49118 x *= 255;
49119 switch (floor(h)) {
49120 case 0:
49121 this.setRGB(c + m, x + m, m);
49122 break;
49123 case 1:
49124 this.setRGB(x + m, c + m, m);
49125 break;
49126 case 2:
49127 this.setRGB(m, c + m, x + m);
49128 break;
49129 case 3:
49130 this.setRGB(m, x + m, c + m);
49131 break;
49132 case 4:
49133 this.setRGB(x + m, m, c + m);
49134 break;
49135 case 5:
49136 this.setRGB(c + m, m, x + m);
49137 break;
49138 }
49139 }
49140 return this;
49141 },
49142
49143 /**
49144 * Return a new color that is lighter than this color.
49145 * @param {Number} [factor=0.2] Lighter factor (0..1).
49146 * @return {Ext.draw.Color}
49147 */
49148 createLighter: function (factor) {
49149 var hsl = this.getHSL();
49150 factor = factor || this.lightnessFactor;
49151 // COMPAT Ext.util.Numbers -> Ext.Number
49152 hsl[2] = hsl[2] + factor;
49153 if (hsl[2] > 1) {
49154 hsl[2] = 1;
49155 } else if (hsl[2] < 0) {
49156 hsl[2] = 0;
49157 }
49158 return Ext.draw.Color.fromHSL(hsl[0], hsl[1], hsl[2]);
49159 },
49160
49161 /**
49162 * Return a new color that is darker than this color.
49163 * @param {Number} [factor=0.2] Darker factor (0..1).
49164 * @return {Ext.draw.Color}
49165 */
49166 createDarker: function (factor) {
49167 factor = factor || this.lightnessFactor;
49168 return this.createLighter(-factor);
49169 },
49170
49171 /**
49172 * Return the color in the hex format, i.e. '#rrggbb'.
49173 * @return {String}
49174 */
49175 toString: function () {
49176 if (this.a === 1) {
49177 var me = this,
49178 round = Math.round,
49179 r = round(me.r).toString(16),
49180 g = round(me.g).toString(16),
49181 b = round(me.b).toString(16);
49182 r = (r.length === 1) ? '0' + r : r;
49183 g = (g.length === 1) ? '0' + g : g;
49184 b = (b.length === 1) ? '0' + b : b;
49185 return ['#', r, g, b].join('');
49186 } else {
49187 return 'rgba(' + [Math.round(this.r), Math.round(this.g), Math.round(this.b), this.a.toFixed(15)].join(',') + ')';
49188 }
49189 },
49190
49191 /**
49192 * Convert a color to hexadecimal format.
49193 *
49194 * @param {String/Array} color The color value (i.e 'rgb(255, 255, 255)', 'color: #ffffff').
49195 * Can also be an Array, in this case the function handles the first member.
49196 * @return {String} The color in hexadecimal format.
49197 */
49198 toHex: function (color) {
49199 if (Ext.isArray(color)) {
49200 color = color[0];
49201 }
49202 if (!Ext.isString(color)) {
49203 return '';
49204 }
49205 if (color.substr(0, 1) === '#') {
49206 return color;
49207 }
49208 var digits = Ext.draw.Color.colorToHexRe.exec(color);
49209
49210 if (Ext.isArray(digits)) {
49211 var red = parseInt(digits[2], 10),
49212 green = parseInt(digits[3], 10),
49213 blue = parseInt(digits[4], 10),
49214 rgb = blue | (green << 8) | (red << 16);
49215 return digits[1] + '#' + ("000000" + rgb.toString(16)).slice(-6);
49216 }
49217 else {
49218 return '';
49219 }
49220 },
49221
49222 /**
49223 * Parse the string and set current color.
49224 *
49225 * Supported formats: '#rrggbb', '#rgb', and 'rgb(r,g,b)'.
49226 *
49227 * If the string is not recognized, an `undefined` will be returned instead.
49228 *
49229 * @param {String} str Color in string.
49230 * @return this
49231 */
49232 setFromString: function (str) {
49233 var values, r, g, b, a = 1,
49234 parse = parseInt;
49235
49236 if (str === 'none') {
49237 this.r = this.g = this.b = this.a = 0;
49238 return this;
49239 }
49240
49241 if ((str.length === 4 || str.length === 7) && str.substr(0, 1) === '#') {
49242 values = str.match(Ext.draw.Color.hexRe);
49243 if (values) {
49244 r = parse(values[1], 16) >> 0;
49245 g = parse(values[2], 16) >> 0;
49246 b = parse(values[3], 16) >> 0;
49247 if (str.length === 4) {
49248 r += (r * 16);
49249 g += (g * 16);
49250 b += (b * 16);
49251 }
49252 }
49253 }
49254 else if ((values = str.match(Ext.draw.Color.rgbToHexRe))) {
49255 r = +values[1];
49256 g = +values[2];
49257 b = +values[3];
49258 } else if ((values = str.match(Ext.draw.Color.rgbaToHexRe))) {
49259 r = +values[1];
49260 g = +values[2];
49261 b = +values[3];
49262 a = +values[4];
49263 } else {
49264 if (Ext.draw.Color.ColorList.hasOwnProperty(str.toLowerCase())) {
49265 return this.setFromString(Ext.draw.Color.ColorList[str.toLowerCase()]);
49266 }
49267 }
49268 if (typeof r === 'undefined') {
49269 return this;
49270 }
49271 this.r = r;
49272 this.g = g;
49273 this.b = b;
49274 this.a = a;
49275 return this;
49276 }
49277 }, function () {
49278 var flyColor = new this();
49279
49280
49281 this.addStatics({
49282 /**
49283 * Returns a flyweight instance of Ext.draw.Color.
49284 *
49285 * Can be called with either a CSS color string or with separate
49286 * arguments for red, green, blue, alpha.
49287 *
49288 * @param {Number/String} red Red component (0..255) or CSS color string.
49289 * @param {Number} [green] Green component (0..255)
49290 * @param {Number} [blue] Blue component (0..255)
49291 * @param {Number} [alpha=1] Alpha component (0..1)
49292 * @return {Ext.draw.Color}
49293 * @static
49294 */
49295 fly: function (r, g, b, a) {
49296 switch (arguments.length) {
49297 case 1:
49298 flyColor.setFromString(r);
49299 break;
49300 case 3:
49301 case 4:
49302 flyColor.setRGB(r, g, b, a);
49303 break;
49304 default:
49305 return null;
49306 }
49307 return flyColor;
49308 },
49309
49310 ColorList: {
49311 "aliceblue": "#f0f8ff", "antiquewhite": "#faebd7", "aqua": "#00ffff", "aquamarine": "#7fffd4", "azure": "#f0ffff",
49312 "beige": "#f5f5dc", "bisque": "#ffe4c4", "black": "#000000", "blanchedalmond": "#ffebcd", "blue": "#0000ff", "blueviolet": "#8a2be2", "brown": "#a52a2a", "burlywood": "#deb887",
49313 "cadetblue": "#5f9ea0", "chartreuse": "#7fff00", "chocolate": "#d2691e", "coral": "#ff7f50", "cornflowerblue": "#6495ed", "cornsilk": "#fff8dc", "crimson": "#dc143c", "cyan": "#00ffff",
49314 "darkblue": "#00008b", "darkcyan": "#008b8b", "darkgoldenrod": "#b8860b", "darkgray": "#a9a9a9", "darkgreen": "#006400", "darkkhaki": "#bdb76b", "darkmagenta": "#8b008b", "darkolivegreen": "#556b2f",
49315 "darkorange": "#ff8c00", "darkorchid": "#9932cc", "darkred": "#8b0000", "darksalmon": "#e9967a", "darkseagreen": "#8fbc8f", "darkslateblue": "#483d8b", "darkslategray": "#2f4f4f", "darkturquoise": "#00ced1",
49316 "darkviolet": "#9400d3", "deeppink": "#ff1493", "deepskyblue": "#00bfff", "dimgray": "#696969", "dodgerblue": "#1e90ff",
49317 "firebrick": "#b22222", "floralwhite": "#fffaf0", "forestgreen": "#228b22", "fuchsia": "#ff00ff",
49318 "gainsboro": "#dcdcdc", "ghostwhite": "#f8f8ff", "gold": "#ffd700", "goldenrod": "#daa520", "gray": "#808080", "green": "#008000", "greenyellow": "#adff2f",
49319 "honeydew": "#f0fff0", "hotpink": "#ff69b4",
49320 "indianred ": "#cd5c5c", "indigo ": "#4b0082", "ivory": "#fffff0", "khaki": "#f0e68c",
49321 "lavender": "#e6e6fa", "lavenderblush": "#fff0f5", "lawngreen": "#7cfc00", "lemonchiffon": "#fffacd", "lightblue": "#add8e6", "lightcoral": "#f08080", "lightcyan": "#e0ffff", "lightgoldenrodyellow": "#fafad2",
49322 "lightgray": "#d3d3d3", "lightgrey": "#d3d3d3", "lightgreen": "#90ee90", "lightpink": "#ffb6c1", "lightsalmon": "#ffa07a", "lightseagreen": "#20b2aa", "lightskyblue": "#87cefa", "lightslategray": "#778899", "lightsteelblue": "#b0c4de",
49323 "lightyellow": "#ffffe0", "lime": "#00ff00", "limegreen": "#32cd32", "linen": "#faf0e6",
49324 "magenta": "#ff00ff", "maroon": "#800000", "mediumaquamarine": "#66cdaa", "mediumblue": "#0000cd", "mediumorchid": "#ba55d3", "mediumpurple": "#9370d8", "mediumseagreen": "#3cb371", "mediumslateblue": "#7b68ee",
49325 "mediumspringgreen": "#00fa9a", "mediumturquoise": "#48d1cc", "mediumvioletred": "#c71585", "midnightblue": "#191970", "mintcream": "#f5fffa", "mistyrose": "#ffe4e1", "moccasin": "#ffe4b5",
49326 "navajowhite": "#ffdead", "navy": "#000080",
49327 "oldlace": "#fdf5e6", "olive": "#808000", "olivedrab": "#6b8e23", "orange": "#ffa500", "orangered": "#ff4500", "orchid": "#da70d6",
49328 "palegoldenrod": "#eee8aa", "palegreen": "#98fb98", "paleturquoise": "#afeeee", "palevioletred": "#d87093", "papayawhip": "#ffefd5", "peachpuff": "#ffdab9", "peru": "#cd853f", "pink": "#ffc0cb", "plum": "#dda0dd", "powderblue": "#b0e0e6", "purple": "#800080",
49329 "red": "#ff0000", "rosybrown": "#bc8f8f", "royalblue": "#4169e1",
49330 "saddlebrown": "#8b4513", "salmon": "#fa8072", "sandybrown": "#f4a460", "seagreen": "#2e8b57", "seashell": "#fff5ee", "sienna": "#a0522d", "silver": "#c0c0c0", "skyblue": "#87ceeb", "slateblue": "#6a5acd", "slategray": "#708090", "snow": "#fffafa", "springgreen": "#00ff7f", "steelblue": "#4682b4",
49331 "tan": "#d2b48c", "teal": "#008080", "thistle": "#d8bfd8", "tomato": "#ff6347", "turquoise": "#40e0d0",
49332 "violet": "#ee82ee",
49333 "wheat": "#f5deb3", "white": "#ffffff", "whitesmoke": "#f5f5f5",
49334 "yellow": "#ffff00", "yellowgreen": "#9acd32"
49335 },
49336
49337 /**
49338 * Create a new color based on the specified HSL values.
49339 *
49340 * @param {Number} h Hue component (0..359)
49341 * @param {Number} s Saturation component (0..1)
49342 * @param {Number} l Lightness component (0..1)
49343 * @return {Ext.draw.Color}
49344 * @static
49345 */
49346 fromHSL: function (h, s, l) {
49347 return (new this(0, 0, 0, 0)).setHSL(h, s, l);
49348 },
49349
49350 /**
49351 * Parse the string and create a new color.
49352 *
49353 * Supported formats: '#rrggbb', '#rgb', and 'rgb(r,g,b)'.
49354 *
49355 * If the string is not recognized, an undefined will be returned instead.
49356 *
49357 * @param {String} string Color in string.
49358 * @returns {Ext.draw.Color}
49359 * @static
49360 */
49361 fromString: function (string) {
49362 return (new this(0, 0, 0, 0)).setFromString(string);
49363 },
49364
49365 /**
49366 * Convenience method for creating a color.
49367 *
49368 * Can be called with several different combinations of arguments:
49369 *
49370 * // Ext.draw.Color is returned unchanged.
49371 * Ext.draw.Color.create(new Ext.draw.color(255, 0, 0, 0));
49372 *
49373 * // CSS color string.
49374 * Ext.draw.Color.create("red");
49375 *
49376 * // Array of red, green, blue, alpha
49377 * Ext.draw.Color.create([255, 0, 0, 0]);
49378 *
49379 * // Separate arguments of red, green, blue, alpha
49380 * Ext.draw.Color.create(255, 0, 0, 0);
49381 *
49382 * // Returns black when no arguments given.
49383 * Ext.draw.Color.create();
49384 *
49385 * @param {Ext.draw.Color/String/Number[]/Number} [red] Red component (0..255),
49386 * CSS color string or array of all components.
49387 * @param {Number} [green] Green component (0..255)
49388 * @param {Number} [blue] Blue component (0..255)
49389 * @param {Number} [alpha=1] Alpha component (0..1)
49390 * @return {Ext.draw.Color}
49391 * @static
49392 */
49393 create: function (arg) {
49394 if (arg instanceof this) {
49395 return arg;
49396 } else if (Ext.isArray(arg)) {
49397 return new Ext.draw.Color(arg[0], arg[1], arg[2], arg[3]);
49398 } else if (Ext.isString(arg)) {
49399 return Ext.draw.Color.fromString(arg);
49400 } else if (arguments.length > 2) {
49401 return new Ext.draw.Color(arguments[0], arguments[1], arguments[2], arguments[3]);
49402 } else {
49403 return new Ext.draw.Color(0, 0, 0, 0);
49404 }
49405 }
49406 });
49407 });
49408 })();
49409
49410 Ext.define('Ext.draw.gradient.GradientDefinition', {
49411 singleton: true,
49412
49413 urlStringRe: /^url\(#([\w\-]+)\)$/,
49414 gradients: {},
49415
49416 add: function (gradients) {
49417 var store = this.gradients,
49418 i, n, gradient;
49419 for (i = 0, n = gradients.length; i < n; i++) {
49420 gradient = gradients[i];
49421 if (Ext.isString(gradient.id)) {
49422 store[gradient.id] = gradient;
49423 }
49424 }
49425 },
49426
49427 get: function (str) {
49428 var store = this.gradients,
49429 match = str.match(this.urlStringRe),
49430 gradient;
49431 if (match && match[1] && (gradient = store[match[1]])) {
49432 return gradient || str;
49433 }
49434 return str;
49435 }
49436 });
49437
49438 /**
49439 * @private
49440 * @class Ext.draw.sprite.AttributeParser
49441 *
49442 * Parsers used for sprite attributes.
49443 */
49444 Ext.define("Ext.draw.sprite.AttributeParser", {
49445 singleton: true,
49446 attributeRe: /^url\(#([a-zA-Z\-]+)\)$/,
49447
49448
49449
49450
49451
49452 "default": function (n) {
49453 return n;
49454 },
49455
49456 string: function (n) {
49457 return String(n);
49458 },
49459
49460 number: function (n) {
49461 if (!isNaN(n)) {
49462 return n;
49463 }
49464 },
49465
49466 angle: function (n) {
49467 if (!isNaN(n)) {
49468 n %= Math.PI * 2;
49469 if (n < -Math.PI) {
49470 n += Math.PI * 2;
49471 }
49472 if (n > Math.PI) {
49473 n -= Math.PI * 2;
49474 }
49475 return n;
49476 }
49477 },
49478
49479 data: function (n) {
49480 if (Ext.isArray(n)) {
49481 return n.slice();
49482 } else if (n instanceof Float32Array) {
49483 return new Float32Array(n);
49484 }
49485 },
49486
49487 bool: function (n) {
49488 return !!n;
49489 },
49490
49491 color: function (n) {
49492 if (n instanceof Ext.draw.Color) {
49493 return n.toString();
49494 } else if (n instanceof Ext.draw.gradient.Gradient) {
49495 return n;
49496 } else if (!n) {
49497 return 'none';
49498 } else if (Ext.isString(n)) {
49499 n = Ext.draw.gradient.GradientDefinition.get(n);
49500 if (Ext.isString(n)) {
49501 return n;
49502 }
49503 }
49504 if (n.type === 'linear') {
49505 return Ext.create('Ext.draw.gradient.Linear', n);
49506 } else if (n.type === 'radial') {
49507 return Ext.create('Ext.draw.gradient.Radial', n);
49508 } else if (n.type === 'pattern') {
49509 return Ext.create('Ext.draw.gradient.Pattern', n);
49510 }
49511 },
49512
49513 limited: function (low, hi) {
49514 return (function (n) {
49515 return isNaN(n) ? undefined : Math.min(Math.max(+n, low), hi);
49516 });
49517 },
49518
49519 limited01: function (n) {
49520 return isNaN(n) ? undefined : Math.min(Math.max(+n, 0), 1);
49521 },
49522
49523 enums: function () {
49524 var enums = {},
49525 args = Array.prototype.slice.call(arguments, 0),
49526 i, ln;
49527
49528 for (i = 0, ln = args.length; i < ln; i++) {
49529 enums[args[i]] = true;
49530 }
49531 return (function (n) {
49532 return n in enums ? n : undefined;
49533 });
49534 }
49535 });
49536
49537 (function () {
49538 function compute(from, to, delta) {
49539 return from + (to - from) * delta;
49540 }
49541
49542 /**
49543 * @private
49544 * @class Ext.draw.sprite.AnimationParser
49545 *
49546 * Parsers for sprite attributes used in animations.
49547 */
49548 Ext.define("Ext.draw.sprite.AnimationParser", {
49549 singleton: true,
49550 attributeRe: /^url\(#([a-zA-Z\-]+)\)$/,
49551
49552
49553 color: {
49554 parseInitial: function (color1, color2) {
49555 if (Ext.isString(color1)) {
49556 color1 = Ext.draw.Color.create(color1);
49557 }
49558 if (Ext.isString(color2)) {
49559 color2 = Ext.draw.Color.create(color2);
49560
49561 }
49562 if ((color1 instanceof Ext.draw.Color) && (color2 instanceof Ext.draw.Color)) {
49563 return [
49564 [color1.r, color1.g, color1.b, color1.a],
49565 [color2.r, color2.g, color2.b, color2.a]
49566 ];
49567 } else {
49568 return [color1 || color2, color2 || color1];
49569 }
49570 },
49571 compute: function (from, to, delta) {
49572 if (!Ext.isArray(from) || !Ext.isArray(to)) {
49573 return to || from;
49574 } else {
49575 return [compute(from[0], to[0], delta), compute(from[1], to[1], delta), compute(from[2], to[2], delta), compute(from[3], to[3], delta)];
49576
49577 }
49578 },
49579 serve: function (array) {
49580 var color = Ext.draw.Color.fly(array[0], array[1], array[2], array[3]);
49581 return color.toString();
49582 }
49583 },
49584
49585 number: {
49586 parse: function (n) {
49587 return n === null ? null : +n;
49588 },
49589
49590 compute: function (from, to, delta) {
49591 if (!Ext.isNumber(from) || !Ext.isNumber(to)) {
49592 return to || from;
49593 } else {
49594 return compute(from, to, delta);
49595 }
49596 }
49597 },
49598
49599 angle: {
49600 parseInitial: function (from, to) {
49601 if (to - from > Math.PI) {
49602 to -= Math.PI * 2;
49603 } else if (to - from < -Math.PI) {
49604 to += Math.PI * 2;
49605 }
49606 return [from, to];
49607 },
49608
49609 compute: function (from, to, delta) {
49610 if (!Ext.isNumber(from) || !Ext.isNumber(to)) {
49611 return to || from;
49612 } else {
49613 return compute(from, to, delta);
49614 }
49615 }
49616 },
49617
49618 path: {
49619 parseInitial: function (from, to) {
49620 var fromStripes = from.toStripes(),
49621 toStripes = to.toStripes(),
49622 i, j,
49623 fromLength = fromStripes.length, toLength = toStripes.length,
49624 fromStripe, toStripe,
49625 length,
49626 lastStripe = toStripes[toLength - 1],
49627 endPoint = [lastStripe[lastStripe.length - 2], lastStripe[lastStripe.length - 1]];
49628 for (i = fromLength; i < toLength; i++) {
49629 fromStripes.push(fromStripes[fromLength - 1].slice(0));
49630 }
49631 for (i = toLength; i < fromLength; i++) {
49632 toStripes.push(endPoint.slice(0));
49633 }
49634 length = fromStripes.length;
49635
49636 toStripes.path = to;
49637 toStripes.temp = new Ext.draw.Path();
49638
49639 for (i = 0; i < length; i++) {
49640 fromStripe = fromStripes[i];
49641 toStripe = toStripes[i];
49642 fromLength = fromStripe.length;
49643 toLength = toStripe.length;
49644 toStripes.temp.types.push('M');
49645 for (j = toLength; j < fromLength; j += 6) {
49646 toStripe.push(endPoint[0], endPoint[1], endPoint[0], endPoint[1], endPoint[0], endPoint[1]);
49647 }
49648
49649 lastStripe = toStripes[toStripes.length - 1];
49650 endPoint = [lastStripe[lastStripe.length - 2], lastStripe[lastStripe.length - 1]];
49651 for (j = fromLength; j < toLength; j += 6) {
49652 fromStripe.push(endPoint[0], endPoint[1], endPoint[0], endPoint[1], endPoint[0], endPoint[1]);
49653 }
49654 for (i = 0; i < toStripe.length; i++) {
49655 toStripe[i] -= fromStripe[i];
49656 }
49657 for (i = 2; i < toStripe.length; i += 6) {
49658 toStripes.temp.types.push('C');
49659 }
49660 }
49661
49662 return [fromStripes, toStripes];
49663 },
49664
49665 compute: function (fromStripes, toStripes, delta) {
49666 if (delta >= 1) {
49667 return toStripes.path;
49668 }
49669 var i = 0, ln = fromStripes.length,
49670 j = 0, ln2, from, to,
49671 temp = toStripes.temp.coords, pos = 0;
49672 for (; i < ln; i++) {
49673 from = fromStripes[i];
49674 to = toStripes[i];
49675 ln2 = from.length;
49676 for (j = 0; j < ln2; j++) {
49677 temp[pos++] = to[j] * delta + from[j];
49678 }
49679 }
49680 return toStripes.temp;
49681 }
49682 },
49683
49684 data: {
49685 compute: function (from, to, delta, target) {
49686 var lf = from.length - 1,
49687 lt = to.length - 1,
49688 len = Math.max(lf, lt),
49689 f, t, i;
49690 if (!target || target === from) {
49691 target = [];
49692 }
49693 target.length = len + 1;
49694 for (i = 0; i <= len; i++) {
49695 f = from[Math.min(i, lf)];
49696 t = to[Math.min(i, lt)];
49697 if (isNaN(f)) {
49698 target[i] = t;
49699 } else {
49700 target[i] = (t - f) * delta + f;
49701 }
49702 }
49703 return target;
49704 }
49705 },
49706
49707 text: {
49708 compute: function (from, to, delta) {
49709 return from.substr(0, Math.round(from.length * (1 - delta))) + to.substr(Math.round(to.length * (1 - delta)));
49710 }
49711 },
49712
49713 limited: "number",
49714 limited01: "number"
49715 });
49716 })();
49717
49718 /**
49719 * @private
49720 * Flyweight object to process the attribute of a sprite.
49721 */
49722 Ext.define("Ext.draw.sprite.AttributeDefinition", {
49723
49724
49725
49726
49727
49728 config: {
49729 /**
49730 * @cfg {Object} defaults Defines the default values of attributes.
49731 */
49732 defaults: {
49733
49734 },
49735
49736 /**
49737 * @cfg {Object} aliases Defines the aletrnative names for attributes.
49738 */
49739 aliases: {
49740
49741 },
49742
49743 /**
49744 * @cfg {Object} animationProcessors Defines the process used to animate between attributes.
49745 */
49746 animationProcessors: {
49747
49748 },
49749
49750 /**
49751 * @cfg {Object} processors Defines the preprocessing used on the attribute.
49752 */
49753 processors: {
49754
49755 },
49756
49757 /**
49758 * @cfg {Object} dirty Defines what other attributes need to be updated when an attribute is changed.
49759 */
49760 dirtyTriggers: {
49761
49762 },
49763
49764 /**
49765 * @cfg {Object} updaters Defines the postprocessing used by the attribute.
49766 */
49767 updaters: {
49768
49769 }
49770 },
49771
49772 inheritableStatics: {
49773 processorRe: /^(\w+)\(([\w\-,]*)\)$/
49774 },
49775
49776 constructor: function (config) {
49777 var me = this;
49778 me.initConfig(config);
49779 },
49780
49781 applyDefaults: function (defaults, oldDefaults) {
49782 oldDefaults = Ext.apply(oldDefaults || {}, this.normalize(defaults));
49783 return oldDefaults;
49784 },
49785
49786 applyAliases: function (aliases, oldAliases) {
49787 return Ext.apply(oldAliases || {}, aliases);
49788 },
49789
49790 applyProcessors: function (processors, oldProcessors) {
49791 this.getAnimationProcessors();
49792 var name,
49793 result = oldProcessors || {},
49794 defaultProcessor = Ext.draw.sprite.AttributeParser,
49795 processorRe = this.self.processorRe,
49796 animationProcessors = {}, anyAnimationProcessors,
49797 match, fn;
49798
49799 for (name in processors) {
49800 fn = processors[name];
49801 if (!Ext.isFunction(fn)) {
49802 if (Ext.isString(fn)) {
49803 match = fn.match(processorRe);
49804 if (match) {
49805 fn = defaultProcessor[match[1]].apply(defaultProcessor, match[2].split(','));
49806 } else {
49807 animationProcessors[name] = fn;
49808 anyAnimationProcessors = true;
49809 fn = defaultProcessor[fn];
49810 }
49811 } else {
49812 continue;
49813 }
49814 }
49815 result[name] = fn;
49816 }
49817
49818 if (anyAnimationProcessors) {
49819 this.setAnimationProcessors(animationProcessors);
49820 }
49821
49822 return result;
49823 },
49824
49825 applyAnimationProcessors: function (animationProcessors, oldAnimationProcessors) {
49826 var parser = Ext.draw.sprite.AnimationParser,
49827 item;
49828
49829 if (!oldAnimationProcessors) {
49830 oldAnimationProcessors = {};
49831 }
49832
49833 for (var name in animationProcessors) {
49834 item = animationProcessors[name];
49835 if (item === 'none') {
49836 oldAnimationProcessors[name] = null;
49837 } else if (Ext.isString(item) && !(name in oldAnimationProcessors)) {
49838 if (item in parser) {
49839 while (Ext.isString(parser[item])) {
49840 item = parser[item];
49841 }
49842 oldAnimationProcessors[name] = parser[item];
49843 }
49844 } else if (Ext.isObject(item)) {
49845 oldAnimationProcessors[name] = item;
49846 }
49847 }
49848 return oldAnimationProcessors;
49849 },
49850
49851 applyDirtyTriggers: function (dirtyTriggers, oldDirtyTrigger) {
49852 if (!oldDirtyTrigger) {
49853 oldDirtyTrigger = {};
49854 }
49855 for (var name in dirtyTriggers) {
49856 oldDirtyTrigger[name] = dirtyTriggers[name].split(',');
49857 }
49858 return oldDirtyTrigger;
49859 },
49860
49861 applyUpdaters: function (updaters, oldUpdaters) {
49862 return Ext.apply(oldUpdaters || {}, updaters);
49863 },
49864
49865 batchedNormalize: function (batchedChanges, reserveUnrecognized) {
49866 if (!batchedChanges) {
49867 return {};
49868 }
49869 var definition = this,
49870 processors = definition.getProcessors(),
49871 aliases = definition.getAliases(),
49872 normalized = {},
49873 i, ln, name, val,
49874 translation, rotation, scaling,
49875 matrix, subVal, split;
49876 if ('rotation' in batchedChanges) {
49877 rotation = batchedChanges.rotation;
49878 }
49879 else {
49880 rotation = ('rotate' in batchedChanges) ? batchedChanges.rotate : undefined;
49881 }
49882
49883 if ('scaling' in batchedChanges) {
49884 scaling = batchedChanges.scaling;
49885 }
49886 else {
49887 scaling = ('scale' in batchedChanges) ? batchedChanges.scale : undefined;
49888 }
49889
49890 if ('translation' in batchedChanges) {
49891 translation = batchedChanges.translation;
49892 } else {
49893 translation = ('translate' in batchedChanges) ? batchedChanges.translate : undefined;
49894 }
49895
49896 if (typeof scaling !== 'undefined') {
49897 if (Ext.isNumber(scaling)) {
49898 normalized.scalingX = scaling;
49899 normalized.scalingY = scaling;
49900 } else {
49901 if ('x' in scaling) {
49902 normalized.scalingX = scaling.x;
49903 }
49904 if ('y' in scaling) {
49905 normalized.scalingY = scaling.y;
49906 }
49907 if ('centerX' in scaling) {
49908 normalized.scalingCenterX = scaling.centerX;
49909 }
49910 if ('centerY' in scaling) {
49911 normalized.scalingCenterY = scaling.centerY;
49912 }
49913 }
49914 }
49915
49916 if (typeof rotation !== 'undefined') {
49917 if (Ext.isNumber(rotation)) {
49918 rotation = Ext.draw.Draw.rad(rotation);
49919 normalized.rotationRads = rotation;
49920 } else {
49921 if ('rads' in rotation) {
49922 normalized.rotationRads = rotation.rads;
49923 } else if ('degrees' in rotation) {
49924 if (Ext.isArray(rotation.degrees)) {
49925 normalized.rotationRads = rotation.degrees.map(function (deg) {
49926 return Ext.draw.Draw.rad(deg);
49927 });
49928 } else {
49929 normalized.rotationRads = Ext.draw.Draw.rad(rotation.degrees);
49930 }
49931 }
49932 if ('centerX' in rotation) {
49933 normalized.rotationCenterX = rotation.centerX;
49934 }
49935 if ('centerY' in rotation) {
49936 normalized.rotationCenterY = rotation.centerY;
49937 }
49938 }
49939 }
49940 if (typeof translation !== 'undefined') {
49941 if ('x' in translation) {
49942 normalized.translationX = translation.x;
49943 }
49944 if ('y' in translation) {
49945 normalized.translationY = translation.y;
49946 }
49947 }
49948
49949 if ('matrix' in batchedChanges) {
49950 matrix = Ext.draw.Matrix.create(batchedChanges.matrix);
49951 split = matrix.split();
49952
49953 normalized.matrix = matrix;
49954 normalized.rotationRads = split.rotation;
49955 normalized.rotationCenterX = 0;
49956 normalized.rotationCenterY = 0;
49957 normalized.scalingX = split.scaleX;
49958 normalized.scalingY = split.scaleY;
49959 normalized.scalingCenterX = 0;
49960 normalized.scalingCenterY = 0;
49961 normalized.translationX = split.translateX;
49962 normalized.translationY = split.translateY;
49963 }
49964
49965 for (name in batchedChanges) {
49966 val = batchedChanges[name];
49967 if (typeof val === 'undefined') {
49968 continue;
49969 } else if (Ext.isArray(val)) {
49970 if (name in aliases) {
49971 name = aliases[name];
49972 }
49973 if (name in processors) {
49974 normalized[name] = [];
49975 for (i = 0, ln = val.length; i < ln; i++) {
49976 subVal = processors[name].call(this, val[i]);
49977 if (typeof subVal !== 'undefined') {
49978 normalized[name][i] = subVal;
49979 }
49980 }
49981 } else if (reserveUnrecognized){
49982 normalized[name] = val;
49983 }
49984 } else {
49985 if (name in aliases) {
49986 name = aliases[name];
49987 }
49988 if (name in processors) {
49989 val = processors[name].call(this, val);
49990 if (typeof val !== 'undefined') {
49991 normalized[name] = val;
49992 }
49993 } else if (reserveUnrecognized){
49994 normalized[name] = val;
49995 }
49996 }
49997 }
49998 return normalized;
49999 },
50000
50001 /**
50002 * Normalizes the changes given via their processors before they are applied as attributes.
50003 *
50004 * @param {Object} changes The changes given.
50005 * @return {Object} The normalized values.
50006 */
50007 normalize: function (changes, reserveUnrecognized) {
50008 if (!changes) {
50009 return {};
50010 }
50011 var definition = this,
50012 processors = definition.getProcessors(),
50013 aliases = definition.getAliases(),
50014 translation = changes.translation || changes.translate,
50015 normalized = {},
50016 name, val, rotation, scaling, matrix, split;
50017
50018 if ('rotation' in changes) {
50019 rotation = changes.rotation;
50020 }
50021 else {
50022 rotation = ('rotate' in changes) ? changes.rotate : undefined;
50023 }
50024
50025 if ('scaling' in changes) {
50026 scaling = changes.scaling;
50027 }
50028 else {
50029 scaling = ('scale' in changes) ? changes.scale : undefined;
50030 }
50031
50032 if (translation) {
50033 if ('x' in translation) {
50034 normalized.translationX = translation.x;
50035 }
50036 if ('y' in translation) {
50037 normalized.translationY = translation.y;
50038 }
50039 }
50040
50041 if (typeof scaling !== 'undefined') {
50042 if (Ext.isNumber(scaling)) {
50043 normalized.scalingX = scaling;
50044 normalized.scalingY = scaling;
50045 } else {
50046 if ('x' in scaling) {
50047 normalized.scalingX = scaling.x;
50048 }
50049 if ('y' in scaling) {
50050 normalized.scalingY = scaling.y;
50051 }
50052 if ('centerX' in scaling) {
50053 normalized.scalingCenterX = scaling.centerX;
50054 }
50055 if ('centerY' in scaling) {
50056 normalized.scalingCenterY = scaling.centerY;
50057 }
50058 }
50059 }
50060
50061 if (typeof rotation !== 'undefined') {
50062 if (Ext.isNumber(rotation)) {
50063 rotation = Ext.draw.Draw.rad(rotation);
50064 normalized.rotationRads = rotation;
50065 } else {
50066 if ('rads' in rotation) {
50067 normalized.rotationRads = rotation.rads;
50068 } else if ('degrees' in rotation) {
50069 normalized.rotationRads = Ext.draw.Draw.rad(rotation.degrees);
50070 }
50071 if ('centerX' in rotation) {
50072 normalized.rotationCenterX = rotation.centerX;
50073 }
50074 if ('centerY' in rotation) {
50075 normalized.rotationCenterY = rotation.centerY;
50076 }
50077 }
50078 }
50079
50080 if ('matrix' in changes) {
50081 matrix = Ext.draw.Matrix.create(changes.matrix);
50082 split = matrix.split();
50083
50084 normalized.matrix = matrix;
50085 normalized.rotationRads = split.rotation;
50086 normalized.rotationCenterX = 0;
50087 normalized.rotationCenterY = 0;
50088 normalized.scalingX = split.scaleX;
50089 normalized.scalingY = split.scaleY;
50090 normalized.scalingCenterX = 0;
50091 normalized.scalingCenterY = 0;
50092 normalized.translationX = split.translateX;
50093 normalized.translationY = split.translateY;
50094 }
50095
50096 for (name in changes) {
50097 val = changes[name];
50098 if (typeof val === 'undefined') {
50099 continue;
50100 }
50101 if (name in aliases) {
50102 name = aliases[name];
50103 }
50104 if (name in processors) {
50105 val = processors[name].call(this, val);
50106 if (typeof val !== 'undefined') {
50107 normalized[name] = val;
50108 }
50109 } else if (reserveUnrecognized){
50110 normalized[name] = val;
50111 }
50112 }
50113 return normalized;
50114 },
50115
50116 setBypassingNormalization: function (attr, modifierStack, changes) {
50117 return modifierStack.pushDown(attr, changes);
50118 },
50119
50120 set: function (attr, modifierStack, changes) {
50121 changes = this.normalize(changes);
50122 return this.setBypassingNormalization(attr, modifierStack, changes);
50123 }
50124 });
50125
50126 /**
50127 * @class Ext.draw.modifier.Modifier
50128 *
50129 * Each sprite has a stack of modifiers. The resulting attributes of sprite is
50130 * the content of the stack top. When setting attributes to a sprite,
50131 * changes will be pushed-down though the stack of modifiers and pop-back the
50132 * additive changes; When modifier is triggered to change the attribute of a
50133 * sprite, it will pop-up the changes to the top.
50134 */
50135 Ext.define("Ext.draw.modifier.Modifier", {
50136 config: {
50137 /**
50138 * @cfg {Ext.draw.modifier.Modifier} previous Previous modifier that receives
50139 * the push-down changes.
50140 */
50141 previous: null,
50142
50143 /**
50144 * @cfg {Ext.draw.modifier.Modifier} next Next modifier that receives the
50145 * pop-up changes.
50146 */
50147 next: null,
50148
50149 /**
50150 * @cfg {Ext.draw.sprite.Sprite} sprite The sprite to which the modifier belongs.
50151 */
50152 sprite: null
50153 },
50154
50155 constructor: function (config) {
50156 this.initConfig(config);
50157 },
50158
50159 updateNext: function (next) {
50160 if (next) {
50161 next.setPrevious(this);
50162 }
50163 },
50164
50165 updatePrev: function (prev) {
50166 if (prev) {
50167 prev.setNext (this);
50168 }
50169 },
50170
50171 /**
50172 * Validate attribute set before use.
50173 *
50174 * @param {Object} attr The attribute to be validated. Note that it may be already initialized, so do
50175 * not override properties that have already been used.
50176 */
50177 prepareAttributes: function (attr) {
50178 if (this._previous) {
50179 this._previous.prepareAttributes(attr);
50180 }
50181 },
50182
50183 /**
50184 * Invoked when changes need to be popped up to the top.
50185 * @param {Object} attributes The source attributes.
50186 * @param {Object} changes The changes to be popped up.
50187 */
50188 popUp: function (attributes, changes) {
50189 if (this._next) {
50190 this._next.popUp(attributes, changes);
50191 } else {
50192 Ext.apply(attributes, changes);
50193 }
50194 },
50195
50196 /**
50197 * Invoked when changes need to be pushed down to the sprite.
50198 * @param {Object} attr The source attributes.
50199 * @param {Object} changes The changes to make. This object might be changed unexpectedly inside the method.
50200 * @return {Mixed}
50201 */
50202 pushDown: function (attr, changes) {
50203 if (this._previous) {
50204 return this._previous.pushDown(attr, changes);
50205 } else {
50206 for (var name in changes) {
50207 if (changes[name] === attr[name]) {
50208 delete changes[name];
50209 }
50210 }
50211 return changes;
50212 }
50213 }
50214 });
50215
50216 /**
50217 * @class Ext.draw.modifier.Target
50218 * @extends Ext.draw.modifier.Modifier
50219 *
50220 * This is the destination modifier that has to be put at
50221 * the top of the modifier stack.
50222 *
50223 */
50224 Ext.define("Ext.draw.modifier.Target", {
50225 extend: Ext.draw.modifier.Modifier ,
50226 alias: 'modifier.target',
50227 statics: {
50228 uniqueId: 0
50229 },
50230
50231 /**
50232 * @inheritdoc
50233 */
50234 prepareAttributes: function (attr) {
50235 if (this._previous) {
50236 this._previous.prepareAttributes(attr);
50237 }
50238 // TODO: Investigate the performance hit for introducing an id
50239 attr.attributeId = 'attribute-' + Ext.draw.modifier.Target.uniqueId++;
50240 if (!attr.hasOwnProperty('canvasAttributes')) {
50241 attr.bbox = {
50242 plain: {dirty: true},
50243 transform: {dirty: true}
50244 };
50245 attr.dirty = true;
50246 attr.dirtyFlags = {};
50247 attr.canvasAttributes = {};
50248 attr.matrix = new Ext.draw.Matrix();
50249 attr.inverseMatrix = new Ext.draw.Matrix();
50250 }
50251 },
50252
50253 /**
50254 * @private
50255 * Applies the appropriate dirty flags from the modifier changes.
50256 * @param {Object} attr The source attributes.
50257 * @param {Object} changes The modifier changes.
50258 */
50259 setDirtyFlags: function (attr, changes) {
50260 Ext.apply(attr, changes);
50261 var sprite = this._sprite,
50262 dirtyTriggers = sprite.self.def._dirtyTriggers,
50263 name, dirtyFlags = attr.dirtyFlags, flags, any = false,
50264 triggers, trigger, i, ln, canvasNames;
50265
50266 for (name in changes) {
50267 if ((triggers = dirtyTriggers[name])) {
50268 i = 0;
50269 while ((trigger = triggers[i++])) {
50270 if (!(flags = dirtyFlags[trigger])) {
50271 flags = dirtyFlags[trigger] = [];
50272 }
50273 flags.push(name);
50274 }
50275 }
50276 }
50277
50278 for (name in changes) {
50279 any = true;
50280 break;
50281 }
50282
50283 if (!any) {
50284 return;
50285 }
50286
50287 // This can prevent sub objects to set duplicated attributes to
50288 // context.
50289 if (dirtyFlags.canvas) {
50290 canvasNames = dirtyFlags.canvas;
50291 delete dirtyFlags.canvas;
50292 for (i = 0, ln = canvasNames.length; i < ln; i++) {
50293 name = canvasNames[i];
50294 attr.canvasAttributes[name] = attr[name];
50295 }
50296 }
50297
50298 // Spreading dirty flags to children
50299 if (attr.hasOwnProperty('children')) {
50300 for (i = 0, ln = attr.children.length; i < ln; i++) {
50301 Ext.apply(attr.children[i].dirtyFlags, dirtyFlags);
50302 sprite.updateDirtyFlags(attr.children[i]);
50303 }
50304 }
50305
50306 sprite.setDirty(true);
50307 },
50308
50309 /**
50310 * @inheritdoc
50311 */
50312 popUp: function (attributes, changes) {
50313 this.setDirtyFlags(attributes, changes);
50314 this._sprite.updateDirtyFlags(attributes);
50315 },
50316
50317 /**
50318 * @inheritdoc
50319 */
50320 pushDown: function (attr, changes) {
50321 if (this._previous) {
50322 changes = this._previous.pushDown(attr, changes);
50323 }
50324 this.setDirtyFlags(attr, changes);
50325 this._sprite.updateDirtyFlags(attr);
50326 return changes;
50327 }
50328 });
50329
50330 (function () {
50331 var pow = Math.pow,
50332 sin = Math.sin,
50333 cos = Math.cos,
50334 sqrt = Math.sqrt,
50335 pi = Math.PI,
50336 easings, addEasing, poly, createPoly, easing, i, l;
50337
50338 //create polynomial easing equations
50339 poly = ['quad', 'cubic', 'quart', 'quint'];
50340
50341 //create other easing equations
50342 easings = {
50343 pow: function (p, x) {
50344 return pow(p, x[0] || 6);
50345 },
50346
50347 expo: function (p) {
50348 return pow(2, 8 * (p - 1));
50349 },
50350
50351 circ: function (p) {
50352 return 1 - sqrt(1 - p * p);
50353 },
50354
50355 sine: function (p) {
50356 return 1 - sin((1 - p) * pi / 2);
50357 },
50358
50359 back: function (p, n) {
50360 n = n || 1.616;
50361 return p * p * ((n + 1) * p - n);
50362 },
50363
50364 bounce: function (p) {
50365 var value;
50366 for (var a = 0, b = 1; 1; a += b, b /= 2) {
50367 if (p >= (7 - 4 * a) / 11) {
50368 value = b * b - pow((11 - 6 * a - 11 * p) / 4, 2);
50369 break;
50370 }
50371 }
50372 return value;
50373 },
50374
50375 elastic: function (p, x) {
50376 return pow(2, 10 * --p) * cos(20 * p * pi * (x || 1) / 3);
50377 }
50378 };
50379
50380 //Add easeIn, easeOut, easeInOut options to all easing equations.
50381 addEasing = function (easing, params) {
50382 params = params && params.length ? params : [ params ];
50383 return Ext.apply(easing, {
50384
50385 easeIn: function (pos) {
50386 return easing(pos, params);
50387 },
50388
50389 easeOut: function (pos) {
50390 return 1 - easing(1 - pos, params);
50391 },
50392
50393 easeInOut: function (pos) {
50394 return (pos <= 0.5) ? easing(2 * pos, params) / 2
50395 : (2 - easing(2 * (1 - pos), params)) / 2;
50396 }
50397 });
50398 };
50399
50400 //Append the polynomial equations with easing support to the EasingPrototype.
50401 createPoly = function (times) {
50402 return function (p) {
50403 return pow(p, times);
50404 };
50405 };
50406
50407 for (i = 0, l = poly.length; i < l; ++i) {
50408 easings[poly[i]] = createPoly(i + 2);
50409 }
50410
50411 //Add linear interpolator
50412 easings.linear = function (x) {
50413 return x;
50414 };
50415
50416 for (easing in easings) {
50417 if (easings.hasOwnProperty(easing)) {
50418 addEasing(easings[easing]);
50419 }
50420 }
50421
50422 /**
50423 * @class
50424 * Contains transition equations such as `Quad`, `Cubic`, `Quart`, `Quint`,
50425 * `Expo`, `Circ`, `Pow`, `Sine`, `Back`, `Bounce`, `Elastic`, etc.
50426 *
50427 * Contains transition equations such as `Quad`, `Cubic`, `Quart`, `Quint`, `Expo`, `Circ`, `Pow`, `Sine`, `Back`, `Bounce`, `Elastic`, etc.
50428 * Each transition also contains methods for applying this function as ease in, ease out or ease in and out accelerations.
50429 *
50430 * var fx = Ext.create('Ext.draw.fx.Sprite', {
50431 * sprite: sprite,
50432 * duration: 1000,
50433 * easing: 'backOut'
50434 * });
50435 */
50436 Ext.define('Ext.draw.TimingFunctions', {
50437 singleton: true,
50438 easingMap: {
50439 linear: easings.linear,
50440 easeIn: easings.quad.easeIn,
50441 easeOut: easings.quad.easeOut,
50442 easeInOut: easings.quad.easeInOut,
50443 backIn: easings.back,
50444 backOut: function (x, n) {
50445 return 1 - easings.back(1 - x, n);
50446 },
50447 backInOut: function (x, n) {
50448 if (x < 0.5) {
50449 return easings.back(x * 2, n) * 0.5;
50450 } else {
50451 return 1 - easings.back((1 - x) * 2, n) * 0.5;
50452 }
50453 },
50454 elasticIn: function (x, n) {
50455 return 1 - easings.elastic(1 - x, n);
50456 },
50457 elasticOut: easings.elastic,
50458 bounceIn: easings.bounce,
50459 bounceOut: function (x) {
50460 return 1 - easings.bounce(1 - x);
50461 }
50462 }
50463 }, function () {
50464 Ext.apply(this, easings);
50465 });
50466
50467 })();
50468
50469
50470 /**
50471 * @class Ext.draw.Animator
50472 *
50473 * Singleton class that manages the animation pool.
50474 */
50475 Ext.define('Ext.draw.Animator', {
50476
50477 singleton: true,
50478
50479 frameCallbacks: {},
50480 frameCallbackId: 0,
50481 scheduled: 0,
50482 frameStartTimeOffset:Date.now(),
50483 animations: [],
50484 running: false,
50485
50486 /**
50487 * Cross platform `animationTime` implementation.
50488 * @return {Number}
50489 */
50490 animationTime: function () {
50491 return Ext.AnimationQueue.frameStartTime - this.frameStartTimeOffset;
50492 },
50493
50494 /**
50495 * Adds an animated object to the animation pool.
50496 *
50497 * @param {Object} animation The animation descriptor to add to the pool.
50498 */
50499 add: function (animation) {
50500 if (!this.contains(animation)) {
50501 this.animations.push(animation);
50502 Ext.draw.Animator.ignite();
50503 if ('fireEvent' in animation) {
50504 animation.fireEvent('animationstart', animation);
50505 }
50506 }
50507 },
50508
50509 /**
50510 * Removes an animation from the pool.
50511 * TODO: This is broken when called within `step` method.
50512 * @param {Object} animation The animation to remove from the pool.
50513 */
50514 remove: function (animation) {
50515 var me = this,
50516 animations = me.animations,
50517 i = 0,
50518 l = animations.length;
50519
50520 for (; i < l; ++i) {
50521 if (animations[i] === animation) {
50522 animations.splice(i, 1);
50523 if ('fireEvent' in animation) {
50524 animation.fireEvent('animationend', animation);
50525 }
50526 return;
50527 }
50528 }
50529 },
50530
50531 /**
50532 * Returns `true` or `false` whether it contains the given animation or not.
50533 *
50534 * @param {Object} animation The animation to check for.
50535 * @return {Boolean}
50536 */
50537 contains: function (animation) {
50538 return this.animations.indexOf(animation) > -1;
50539 },
50540
50541 /**
50542 * Returns `true` or `false` whether the pool is empty or not.
50543 * @return {Boolean}
50544 */
50545 empty: function () {
50546 return this.animations.length === 0;
50547 },
50548
50549 /**
50550 * Given a frame time it will filter out finished animations from the pool.
50551 *
50552 * @param {Number} frameTime The frame's start time, in milliseconds.
50553 */
50554 step: function (frameTime) {
50555 var me = this,
50556 animations = me.animations,
50557 animation,
50558 i = 0,
50559 ln = animations.length;
50560
50561 for (; i < ln; i++) {
50562 animation = animations[i];
50563 animation.step(frameTime);
50564 if (!animation.animating) {
50565 animations.splice(i, 1);
50566 i--;
50567 ln--;
50568 if (animation.fireEvent) {
50569 animation.fireEvent('animationend');
50570 }
50571 }
50572 }
50573 },
50574
50575 /**
50576 * Register an one-time callback that will be called at the next frame.
50577 * @param {Function} callback
50578 * @param {Object} scope
50579 * @return {String}
50580 */
50581 schedule: function (callback, scope) {
50582 scope = scope || this;
50583 var id = 'frameCallback' + (this.frameCallbackId++);
50584
50585 if (Ext.isString(callback)) {
50586 callback = scope[callback];
50587 }
50588 Ext.draw.Animator.frameCallbacks[id] = {fn: callback, scope: scope, once: true};
50589 this.scheduled++;
50590 Ext.draw.Animator.ignite();
50591 return id;
50592 },
50593
50594 /**
50595 * Cancel a registered one-time callback
50596 * @param {String} id
50597 */
50598 cancel: function (id) {
50599 if (Ext.draw.Animator.frameCallbacks[id] && Ext.draw.Animator.frameCallbacks[id].once) {
50600 this.scheduled--;
50601 delete Ext.draw.Animator.frameCallbacks[id];
50602 }
50603 },
50604
50605 /**
50606 * Register a recursive callback that will be called at every frame.
50607 *
50608 * @param {Function} callback
50609 * @param {Object} scope
50610 * @return {String}
50611 */
50612 addFrameCallback: function (callback, scope) {
50613 scope = scope || this;
50614 if (Ext.isString(callback)) {
50615 callback = scope[callback];
50616 }
50617 var id = 'frameCallback' + (this.frameCallbackId++);
50618
50619 Ext.draw.Animator.frameCallbacks[id] = {fn: callback, scope: scope};
50620 return id;
50621 },
50622
50623 /**
50624 * Unregister a recursive callback.
50625 * @param {String} id
50626 */
50627 removeFrameCallback: function (id) {
50628 delete Ext.draw.Animator.frameCallbacks[id];
50629 },
50630
50631 /**
50632 * @private
50633 */
50634 fireFrameCallbacks: function () {
50635 var callbacks = this.frameCallbacks,
50636 id, fn, cb;
50637
50638 for (id in callbacks) {
50639 cb = callbacks[id];
50640 fn = cb.fn;
50641 if (Ext.isString(fn)) {
50642 fn = cb.scope[fn];
50643 }
50644
50645 fn.call(cb.scope);
50646
50647 if (callbacks[id] && cb.once) {
50648 this.scheduled--;
50649 delete callbacks[id];
50650 }
50651 }
50652 },
50653
50654 handleFrame: function() {
50655 this.step(this.animationTime());
50656 this.fireFrameCallbacks();
50657 if (!this.scheduled && this.empty()) {
50658 Ext.AnimationQueue.stop(this.handleFrame, this);
50659 this.running = false;
50660 }
50661 },
50662
50663 ignite: function() {
50664 if (!this.running) {
50665 this.running = true;
50666 Ext.AnimationQueue.start(this.handleFrame, this);
50667 Ext.draw.Draw.updateIOS();
50668 }
50669 }
50670 });
50671
50672 /**
50673 * The Animation modifier.
50674 *
50675 * Sencha Touch allows users to use transitional animation on sprites. Simply set the duration
50676 * and easing in the animation modifier, then all the changes to the sprites will be animated.
50677 *
50678 * Also, you can use different durations and easing functions on different attributes by using
50679 * {@link #customDuration} and {@link #customEasings}.
50680 *
50681 * By default, an animation modifier will be created during the initialization of a sprite.
50682 * You can get the modifier of `sprite` by `sprite.fx`.
50683 *
50684 */
50685 Ext.define("Ext.draw.modifier.Animation", {
50686 mixins: {
50687 observable: Ext.mixin.Observable
50688 },
50689
50690
50691
50692
50693 extend: Ext.draw.modifier.Modifier ,
50694 alias: 'modifier.animation',
50695
50696 config: {
50697 /**
50698 * @cfg {Function} easing
50699 * Default easing function.
50700 */
50701 easing: function (x) {
50702 return x;
50703 },
50704
50705 /**
50706 * @cfg {Number} duration
50707 * Default duration time (ms).
50708 */
50709 duration: 0,
50710
50711 /**
50712 * @cfg {Object} customEasings Overrides the default easing function for defined attributes.
50713 */
50714 customEasings: {},
50715
50716 /**
50717 * @cfg {Object} customDuration Overrides the default duration for defined attributes.
50718 */
50719 customDuration: {}
50720 },
50721
50722 constructor: function () {
50723 this.anyAnimation = false;
50724 this.anySpecialAnimations = false;
50725 this.animating = 0;
50726 this.animatingPool = [];
50727 this.callSuper(arguments);
50728 },
50729
50730 /**
50731 * @inheritdoc
50732 */
50733 prepareAttributes: function (attr) {
50734 if (!attr.hasOwnProperty('timers')) {
50735 attr.animating = false;
50736 attr.timers = {};
50737 attr.animationOriginal = Ext.Object.chain(attr);
50738 attr.animationOriginal.upperLevel = attr;
50739 }
50740 if (this._previous) {
50741 this._previous.prepareAttributes(attr.animationOriginal);
50742 }
50743 },
50744
50745 updateSprite: function (sprite) {
50746 // Apply the config that was configured in the sprite.
50747 this.setConfig(sprite.config.fx);
50748 },
50749
50750 updateDuration: function (duration) {
50751 this.anyAnimation = duration > 0;
50752 },
50753
50754 applyEasing: function (easing) {
50755 if (typeof easing === 'string') {
50756 return Ext.draw.TimingFunctions.easingMap[easing];
50757 } else {
50758 return easing;
50759 }
50760 },
50761
50762 applyCustomEasings: function (newCustomEasing, oldCustomEasing) {
50763 oldCustomEasing = oldCustomEasing || {};
50764 var attr, attrs, easing, i, ln;
50765
50766 for (attr in newCustomEasing) {
50767 easing = newCustomEasing[attr];
50768 attrs = attr.split(',');
50769 if (typeof easing === 'string') {
50770 easing = Ext.draw.TimingFunctions.easingMap[easing];
50771 }
50772 for (i = 0, ln = attrs.length; i < ln; i++) {
50773 oldCustomEasing[attrs[i]] = easing;
50774 }
50775 }
50776 return oldCustomEasing;
50777 },
50778
50779 /**
50780 * Set special easings on the given attributes. E.g.:
50781 *
50782 * circleSprite.fx.setEasingOn('r', 'elasticIn');
50783 *
50784 * @param {String/Array} attrs The source attribute(s).
50785 * @param {String} easing The special easings.
50786 */
50787 setEasingOn: function (attrs, easing) {
50788 attrs = Ext.Array.from(attrs).slice();
50789 var customEasings = {},
50790 i = 0,
50791 ln = attrs.length;
50792
50793 for (; i < ln; i++) {
50794 customEasings[attrs[i]] = easing;
50795 }
50796 this.setCustomEasings(customEasings);
50797 },
50798
50799 /**
50800 * Remove special easings on the given attributes.
50801 * @param {String/Array} attrs The source attribute(s).
50802 */
50803 clearEasingOn: function (attrs) {
50804 attrs = Ext.Array.from(attrs, true);
50805 var i = 0, ln = attrs.length;
50806 for (; i < ln; i++) {
50807 delete this._customEasings[attrs[i]];
50808 }
50809 },
50810
50811 applyCustomDuration: function (newCustomDuration, oldCustomDuration) {
50812 oldCustomDuration = oldCustomDuration || {};
50813 var attr, duration, attrs, i, ln, anySpecialAnimations = this.anySpecialAnimations;
50814
50815 for (attr in newCustomDuration) {
50816 duration = newCustomDuration[attr];
50817 attrs = attr.split(',');
50818 anySpecialAnimations = true;
50819
50820 for (i = 0, ln = attrs.length; i < ln; i++) {
50821 oldCustomDuration[attrs[i]] = duration;
50822 }
50823 }
50824 this.anySpecialAnimations = anySpecialAnimations;
50825 return oldCustomDuration;
50826 },
50827
50828 /**
50829 * Set special duration on the given attributes. E.g.:
50830 *
50831 * rectSprite.fx.setDurationOn('height', 2000);
50832 *
50833 * @param {String/Array} attrs The source attributes.
50834 * @param {Number} duration The special duration.
50835 */
50836 setDurationOn: function (attrs, duration) {
50837 attrs = Ext.Array.from(attrs).slice();
50838 var customDurations = {},
50839 i = 0,
50840 ln = attrs.length;
50841
50842 for (; i < ln; i++) {
50843 customDurations[attrs[i]] = duration;
50844 }
50845 this.setCustomDuration(customDurations);
50846 },
50847
50848 /**
50849 * Remove special easings on the given attributes.
50850 * @param {Object} attrs The source attributes.
50851 */
50852 clearDurationOn: function (attrs) {
50853 attrs = Ext.Array.from(attrs, true);
50854 var i = 0, ln = attrs.length;
50855
50856 for (; i < ln; i++) {
50857 delete this._customDuration[attrs[i]];
50858 }
50859 },
50860
50861 /**
50862 * @private
50863 * Initializes Animator for the animation.
50864 * @param {Object} attributes The source attributes.
50865 * @param {String} animating The animating flag.
50866 */
50867 setAnimating: function (attributes, animating) {
50868 var me = this,
50869 i, j;
50870
50871 if (attributes.animating !== animating) {
50872 attributes.animating = animating;
50873 if (animating) {
50874 me.animatingPool.push(attributes);
50875 if (me.animating === 0) {
50876 Ext.draw.Animator.add(me);
50877 }
50878 me.animating++;
50879 } else {
50880 for (i = 0, j = 0; i < me.animatingPool.length; i++) {
50881 if (me.animatingPool[i] !== attributes) {
50882 me.animatingPool[j++] = me.animatingPool[i];
50883 }
50884 }
50885 me.animating = me.animatingPool.length = j;
50886 }
50887 }
50888 },
50889
50890 /**
50891 * @private
50892 * Set the attr with given easing and duration.
50893 * @param {Object} attr The attributes collection.
50894 * @param {Object} changes The changes that popped up from lower modifier.
50895 * @return {Object} The changes to pop up.
50896 */
50897 setAttrs: function (attr, changes) {
50898 var timers = attr.timers,
50899 parsers = this._sprite.self.def._animationProcessors,
50900 defaultEasing = this._easing,
50901 defaultDuration = this._duration,
50902 customDuration = this._customDuration,
50903 customEasings = this._customEasings,
50904 anySpecial = this.anySpecialAnimations,
50905 any = this.anyAnimation || anySpecial,
50906 original = attr.animationOriginal,
50907 ignite = false,
50908 timer, name, newValue, startValue, parser, easing, duration;
50909
50910 if (!any) {
50911 // If there is no animation enabled
50912 // When applying changes to attributes, simply stop current animation
50913 // and set the value.
50914 for (name in changes) {
50915 if (attr[name] === changes[name]) {
50916 delete changes[name];
50917 } else {
50918 attr[name] = changes[name];
50919 }
50920 delete original[name];
50921 delete timers[name];
50922 }
50923 return changes;
50924 } else {
50925 // If any animation
50926 for (name in changes) {
50927 newValue = changes[name];
50928 startValue = attr[name];
50929 if (newValue !== startValue && any && startValue !== undefined && startValue !== null && (parser = parsers[name])) {
50930 // If this property is animating.
50931
50932 // Figure out the desired duration and easing.
50933 easing = defaultEasing;
50934 duration = defaultDuration;
50935 if (anySpecial) {
50936 // Deducing the easing function and duration
50937 if (name in customEasings) {
50938 easing = customEasings[name];
50939 }
50940 if (name in customDuration) {
50941 duration = customDuration[name];
50942 }
50943 }
50944
50945 // If the property is animating
50946 if (duration) {
50947 if (!timers[name]) {
50948 timers[name] = {};
50949 }
50950
50951 timer = timers[name];
50952 timer.start = 0;
50953 timer.easing = easing;
50954 timer.duration = duration;
50955 timer.compute = parser.compute;
50956 timer.serve = parser.serve || Ext.draw.Draw.reflectFn;
50957
50958 if (parser.parseInitial) {
50959 var initial = parser.parseInitial(startValue, newValue);
50960 timer.source = initial[0];
50961 timer.target = initial[1];
50962 } else if (parser.parse) {
50963 timer.source = parser.parse(startValue);
50964 timer.target = parser.parse(newValue);
50965 } else {
50966 timer.source = startValue;
50967 timer.target = newValue;
50968 }
50969 // The animation started. Change to originalVal.
50970 timers[name] = timer;
50971 original[name] = newValue;
50972 delete changes[name];
50973 ignite = true;
50974 continue;
50975 } else {
50976 delete original[name];
50977 }
50978 } else {
50979 delete original[name];
50980 }
50981
50982 // If the property is not animating.
50983 delete timers[name];
50984 }
50985 }
50986
50987 if (ignite && !attr.animating) {
50988 this.setAnimating(attr, true);
50989 }
50990
50991 return changes;
50992 },
50993
50994 /**
50995 * @private
50996 *
50997 * Update attributes to current value according to current animation time.
50998 * This method will not effect the values of lower layers, but may delete a
50999 * value from it.
51000 * @param {Object} attr The source attributes.
51001 * @return {Object} the changes to popup.
51002 */
51003 updateAttributes: function (attr) {
51004 if (!attr.animating) {
51005 return {};
51006 }
51007 var changes = {},
51008 any = false,
51009 original = attr.animationOriginal,
51010 timers = attr.timers,
51011 now = Ext.draw.Animator.animationTime(),
51012 name, timer, delta;
51013
51014 // If updated in the same frame, return.
51015 if (attr.lastUpdate === now) {
51016 return {};
51017 }
51018
51019 for (name in timers) {
51020 timer = timers[name];
51021 if (!timer.start) {
51022 timer.start = now;
51023 delta = 0;
51024 } else {
51025 delta = (now - timer.start) / timer.duration;
51026 }
51027 if (delta >= 1) {
51028 changes[name] = original[name];
51029 delete original[name];
51030 delete timers[name];
51031 } else {
51032 changes[name] = timer.serve(timer.compute(timer.source, timer.target, timer.easing(delta), attr[name]));
51033 any = true;
51034 }
51035 }
51036 attr.lastUpdate = now;
51037 this.setAnimating(attr, any);
51038 return changes;
51039 },
51040
51041 /**
51042 * @inheritdoc
51043 */
51044 pushDown: function (attr, changes) {
51045 changes = Ext.draw.modifier.Modifier.prototype.pushDown.call(this, attr.animationOriginal, changes);
51046 return this.setAttrs(attr, changes);
51047 },
51048
51049 /**
51050 * @inheritdoc
51051 */
51052 popUp: function (attr, changes) {
51053 attr = attr.upperLevel;
51054 changes = this.setAttrs(attr, changes);
51055 if (this._next) {
51056 return this._next.popUp(attr, changes);
51057 } else {
51058 return Ext.apply(attr, changes);
51059 }
51060 },
51061
51062 // This is called as an animated object in `Ext.draw.Animator`.
51063 step: function () {
51064 var me = this,
51065 pool = me.animatingPool.slice(),
51066 attributes,
51067 i, ln;
51068
51069 for (i = 0, ln = pool.length; i < ln; i++) {
51070 attributes = pool[i];
51071 var changes = this.updateAttributes(attributes),
51072 name;
51073
51074 // Looking for anything in changes
51075 //noinspection LoopStatementThatDoesntLoopJS
51076 for (name in changes) {
51077 if (this._next) {
51078 this._next.popUp(attributes, changes);
51079 }
51080 break;
51081 }
51082 }
51083 },
51084
51085 /**
51086 * Stop all animations effected by this modifier
51087 */
51088 stop: function () {
51089 this.step();
51090
51091 var me = this,
51092 pool = me.animatingPool,
51093 i, ln;
51094
51095 for (i = 0, ln = pool.length; i < ln; i++) {
51096 pool[i].animating = false;
51097 }
51098 me.animatingPool.length = 0;
51099 me.animating = 0;
51100 Ext.draw.Animator.remove(me);
51101 },
51102
51103 destroy: function () {
51104 var me = this;
51105 me.animatingPool.length = 0;
51106 me.animating = 0;
51107 }
51108 });
51109
51110 /**
51111 * @class Ext.draw.modifier.Highlight
51112 * @extends Ext.draw.modifier.Modifier
51113 *
51114 * Highlight is a modifier that will override the attributes
51115 * with its `highlightStyle` attributes when `highlighted` is true.
51116 */
51117 Ext.define("Ext.draw.modifier.Highlight", {
51118 extend: Ext.draw.modifier.Modifier ,
51119 alias: 'modifier.highlight',
51120
51121 config: {
51122
51123 /**
51124 * @cfg {Boolean} enabled 'true' if the highlight is applied.
51125 */
51126 enabled: false,
51127
51128 /**
51129 * @cfg {Object} highlightStyle The style attributes of the highlight modifier.
51130 */
51131 highlightStyle: null
51132 },
51133
51134 preFx: true,
51135
51136 applyHighlightStyle: function (style, oldStyle) {
51137 oldStyle = oldStyle || {};
51138 if (this.getSprite()) {
51139 Ext.apply(oldStyle, this.getSprite().self.def.normalize(style));
51140 } else {
51141 Ext.apply(oldStyle, style);
51142 }
51143 return oldStyle;
51144 },
51145
51146 /**
51147 * @inheritdoc
51148 */
51149 prepareAttributes: function (attr) {
51150 if (!attr.hasOwnProperty('highlightOriginal')) {
51151 attr.highlighted = false;
51152 attr.highlightOriginal = Ext.Object.chain(attr);
51153 }
51154 if (this._previous) {
51155 this._previous.prepareAttributes(attr.highlightOriginal);
51156 }
51157 },
51158
51159 updateSprite: function (sprite, oldSprite) {
51160 if (sprite) {
51161 if (this.getHighlightStyle()) {
51162 this._highlightStyle = sprite.self.def.normalize(this.getHighlightStyle());
51163 }
51164 this.setHighlightStyle(sprite.config.highlightCfg);
51165 }
51166
51167 // Before attaching to a sprite, register the highlight related
51168 // attributes to its definition.
51169 //
51170 // TODO(zhangbei): Unfortunately this will effect all the sprites of the same type.
51171 // As the redundant attributes would not effect performance, it is not yet a big problem.
51172 var def = sprite.self.def;
51173 this.setSprite(sprite);
51174 def.setConfig({
51175 defaults: {
51176 highlighted: false
51177 },
51178
51179 processors: {
51180 highlighted: 'bool'
51181 },
51182
51183 aliases: {
51184 "highlight": "highlighted",
51185 "highlighting": "highlighted"
51186 },
51187
51188 dirtyFlags: {
51189 },
51190
51191 updaters: {
51192
51193 }
51194 });
51195 },
51196
51197 /**
51198 * Filter modifier changes if overriding source attributes.
51199 * @param {Object} attr The source attributes.
51200 * @param {Object} changes The modifier changes.
51201 * @return {*} The filtered changes.
51202 */
51203 filterChanges: function (attr, changes) {
51204 var me = this,
51205 name,
51206 original = attr.highlightOriginal,
51207 style = me.getHighlightStyle();
51208 if (attr.highlighted) {
51209 for (name in changes) {
51210 if (style.hasOwnProperty(name)) {
51211 // If it's highlighted, then save the changes to lower level
51212 // on overridden attributes.
51213 original[name] = changes[name];
51214 delete changes[name];
51215 }
51216 }
51217 }
51218
51219 for (name in changes) {
51220 if (name !== 'highlighted' && original[name] === changes[name]) {
51221 // If it's highlighted, then save the changes to lower level
51222 // on overridden attributes.
51223 delete changes[name];
51224 }
51225 }
51226
51227 return changes;
51228 },
51229
51230 /**
51231 * @inheritdoc
51232 */
51233 pushDown: function (attr, changes) {
51234 var style = this.getHighlightStyle(),
51235 original = attr.highlightOriginal,
51236 oldHighlighted, name;
51237
51238 if (changes.hasOwnProperty('highlighted')) {
51239 oldHighlighted = changes.highlighted;
51240 // Hide `highlighted` and `highlightStyle` to underlying modifiers.
51241 delete changes.highlighted;
51242
51243 if (this._previous) {
51244 changes = this._previous.pushDown(original, changes);
51245 }
51246 changes = this.filterChanges(attr, changes);
51247
51248 if (oldHighlighted !== attr.highlighted) {
51249 if (oldHighlighted) {
51250 // switching on
51251 // At this time, original should be empty.
51252 for (name in style) {
51253 // If changes[name] just changed the value in lower levels,
51254 if (name in changes) {
51255 original[name] = changes[name];
51256 } else {
51257 original[name] = attr[name];
51258 }
51259 if (original[name] !== style[name]) {
51260 changes[name] = style[name];
51261 }
51262 }
51263 } else {
51264 // switching off
51265 for (name in style) {
51266 if (!(name in changes)) {
51267 changes[name] = original[name];
51268 }
51269 delete original[name]; // TODO: Need deletion API?
51270 }
51271 }
51272 changes.highlighted = oldHighlighted;
51273 }
51274 } else {
51275 if (this._previous) {
51276 changes = this._previous.pushDown(original, changes);
51277 }
51278 changes = this.filterChanges(attr, changes);
51279 }
51280
51281 return changes;
51282 },
51283
51284 /**
51285 * @inheritdoc
51286 */
51287 popUp: function (attr, changes) {
51288 changes = this.filterChanges(attr, changes);
51289 Ext.draw.modifier.Modifier.prototype.popUp.call(this, attr, changes);
51290 }
51291 });
51292
51293 /**
51294 * A sprite is an object rendered in a drawing {@link Ext.draw.Surface}.
51295 * The Sprite class itself is an abstract class and is not meant to be used directly.
51296 * Every sprite in the Draw and Chart packages is a subclass of the Ext.draw.sprite.Sprite.
51297 * The standard Sprite subclasses are:
51298 *
51299 * * {@link Ext.draw.sprite.Path} - A sprite that represents a path.
51300 * * {@link Ext.draw.sprite.Rect} - A sprite that represents a rectangle.
51301 * * {@link Ext.draw.sprite.Circle} - A sprite that represents a circle.
51302 * * {@link Ext.draw.sprite.Sector} - A sprite representing a pie slice.
51303 * * {@link Ext.draw.sprite.Arc} - A sprite that represents a circular arc.
51304 * * {@link Ext.draw.sprite.Ellipse} - A sprite that represents an ellipse.
51305 * * {@link Ext.draw.sprite.EllipticalArc} - A sprite that represents an elliptical arc.
51306 * * {@link Ext.draw.sprite.Text} - A sprite that represents text.
51307 * * {@link Ext.draw.sprite.Image} - A sprite that represents an image.
51308 * * {@link Ext.draw.sprite.Instancing} - A sprite that represents multiple instances based on the given template.
51309 * * {@link Ext.draw.sprite.Composite} - Represents a group of sprites.
51310 *
51311 * Sprites can be created with a reference to a {@link Ext.draw.Surface}
51312 *
51313 * var drawComponent = Ext.create('Ext.draw.Component', {
51314 * // ...
51315 * });
51316 *
51317 * var sprite = Ext.create('Ext.draw.sprite.Sprite', {
51318 * type: 'circle',
51319 * fill: '#ff0',
51320 * surface: drawComponent.getSurface('main'),
51321 * radius: 5
51322 * });
51323 *
51324 * Sprites can also be added to the surface as a configuration object:
51325 *
51326 * var sprite = drawComponent.getSurface('main').add({
51327 * type: 'circle',
51328 * fill: '#ff0',
51329 * radius: 5
51330 * });
51331 */
51332 Ext.define('Ext.draw.sprite.Sprite', {
51333 alias: 'sprite.sprite',
51334
51335 mixins: {
51336 observable: Ext.mixin.Observable
51337 },
51338
51339
51340
51341
51342
51343
51344
51345
51346
51347
51348
51349 isSprite: true,
51350
51351 inheritableStatics: {
51352 def: {
51353 processors: {
51354 /**
51355 * @cfg {String} [strokeStyle="none"] The color of the stroke (a CSS color value).
51356 */
51357 strokeStyle: "color",
51358
51359 /**
51360 * @cfg {String} [fillStyle="none"] The color of the shape (a CSS color value).
51361 */
51362 fillStyle: "color",
51363
51364 /**
51365 * @cfg {Number} [strokeOpacity=1] The opacity of the stroke. Limited from 0 to 1.
51366 */
51367 strokeOpacity: "limited01",
51368
51369 /**
51370 * @cfg {Number} [fillOpacity=1] The opacity of the fill. Limited from 0 to 1.
51371 */
51372 fillOpacity: "limited01",
51373
51374 /**
51375 * @cfg {Number} [lineWidth=1] The width of the line stroke.
51376 */
51377 lineWidth: "number",
51378
51379 /**
51380 * @cfg {String} [lineCap="butt"] The style of the line caps.
51381 */
51382 lineCap: "enums(butt,round,square)",
51383
51384 /**
51385 * @cfg {String} [lineJoin="miter"] The style of the line join.
51386 */
51387 lineJoin: "enums(round,bevel,miter)",
51388
51389 /**
51390 * @cfg {Array} An array of non-negative numbers specifying a dash/space sequence.
51391 */
51392 lineDash: "data",
51393
51394 /**
51395 * @cfg {Number} A number specifying how far into the line dash sequence drawing commences.
51396 */
51397 lineDashOffset: "number",
51398
51399 /**
51400 * @cfg {Number} [miterLimit=1] Sets the distance between the inner corner and the outer corner where two lines meet.
51401 */
51402 miterLimit: "number",
51403
51404 /**
51405 * @cfg {String} [shadowColor="none"] The color of the shadow (a CSS color value).
51406 */
51407 shadowColor: "color",
51408
51409 /**
51410 * @cfg {Number} [shadowOffsetX=0] The offset of the sprite's shadow on the x-axis.
51411 */
51412 shadowOffsetX: "number",
51413
51414 /**
51415 * @cfg {Number} [shadowOffsetY=0] The offset of the sprite's shadow on the y-axis.
51416 */
51417 shadowOffsetY: "number",
51418
51419 /**
51420 * @cfg {Number} [shadowBlur=0] The amount blur used on the shadow.
51421 */
51422 shadowBlur: "number",
51423
51424 /**
51425 * @cfg {Number} [globalAlpha=1] The opacity of the sprite. Limited from 0 to 1.
51426 */
51427 globalAlpha: "limited01",
51428 globalCompositeOperation: "enums(source-over,destination-over,source-in,destination-in,source-out,destination-out,source-atop,destination-atop,lighter,xor,copy)",
51429
51430 /**
51431 * @cfg {Boolean} [hidden=false] Determines whether or not the sprite is hidden.
51432 */
51433 hidden: "bool",
51434
51435 /**
51436 * @cfg {Boolean} [transformFillStroke=false] Determines whether the fill and stroke are affected by sprite transformations.
51437 */
51438 transformFillStroke: "bool",
51439
51440 /**
51441 * @cfg {Number} [zIndex=0] The stacking order of the sprite.
51442 */
51443 zIndex: "number",
51444
51445 /**
51446 * @cfg {Number} [translationX=0] The translation of the sprite on the x-axis.
51447 */
51448 translationX: "number",
51449
51450 /**
51451 * @cfg {Number} [translationY=0] The translation of the sprite on the y-axis.
51452 */
51453 translationY: "number",
51454
51455 /**
51456 * @cfg {Number} [rotationRads=0] The degree of rotation of the sprite.
51457 */
51458 rotationRads: "number",
51459
51460 /**
51461 * @cfg {Number} [rotationCenterX=null] The central coordinate of the sprite's scale operation on the x-axis.
51462 */
51463 rotationCenterX: "number",
51464
51465 /**
51466 * @cfg {Number} [rotationCenterY=null] The central coordinate of the sprite's rotate operation on the y-axis.
51467 */
51468 rotationCenterY: "number",
51469
51470 /**
51471 * @cfg {Number} [scalingX=1] The scaling of the sprite on the x-axis.
51472 */
51473 scalingX: "number",
51474
51475 /**
51476 * @cfg {Number} [scalingY=1] The scaling of the sprite on the y-axis.
51477 */
51478 scalingY: "number",
51479
51480 /**
51481 * @cfg {Number} [scalingCenterX=null] The central coordinate of the sprite's scale operation on the x-axis.
51482 */
51483 scalingCenterX: "number",
51484
51485 /**
51486 * @cfg {Number} [scalingCenterY=null] The central coordinate of the sprite's scale operation on the y-axis.
51487 */
51488 scalingCenterY: "number",
51489
51490 constrainGradients: "bool"
51491 },
51492
51493 aliases: {
51494 "stroke": "strokeStyle",
51495 "fill": "fillStyle",
51496 "color": "fillStyle",
51497 "stroke-width": "lineWidth",
51498 "stroke-linecap": "lineCap",
51499 "stroke-linejoin": "lineJoin",
51500 "stroke-miterlimit": "miterLimit",
51501 "text-anchor": "textAlign",
51502 "opacity": "globalAlpha",
51503
51504 translateX: "translationX",
51505 translateY: "translationY",
51506 rotateRads: "rotationRads",
51507 rotateCenterX: "rotationCenterX",
51508 rotateCenterY: "rotationCenterY",
51509 scaleX: "scalingX",
51510 scaleY: "scalingY",
51511 scaleCenterX: "scalingCenterX",
51512 scaleCenterY: "scalingCenterY"
51513 },
51514
51515 defaults: {
51516 hidden: false,
51517 zIndex: 0,
51518
51519 strokeStyle: "none",
51520 fillStyle: "none",
51521 lineWidth: 1,
51522 lineDash: [],
51523 lineDashOffset: 0,
51524 lineCap: "butt",
51525 lineJoin: "miter",
51526 miterLimit: 1,
51527
51528 shadowColor: "none",
51529 shadowOffsetX: 0,
51530 shadowOffsetY: 0,
51531 shadowBlur: 0,
51532
51533 globalAlpha: 1,
51534 strokeOpacity: 1,
51535 fillOpacity: 1,
51536 transformFillStroke: false,
51537
51538 translationX: 0,
51539 translationY: 0,
51540 rotationRads: 0,
51541 rotationCenterX: null,
51542 rotationCenterY: null,
51543 scalingX: 1,
51544 scalingY: 1,
51545 scalingCenterX: null,
51546 scalingCenterY: null,
51547
51548 constrainGradients: false
51549 },
51550
51551 dirtyTriggers: {
51552 hidden: "canvas",
51553 zIndex: "zIndex",
51554
51555 globalAlpha: "canvas",
51556 globalCompositeOperation: "canvas",
51557
51558 transformFillStroke: "canvas",
51559 strokeStyle: "canvas",
51560 fillStyle: "canvas",
51561 strokeOpacity: "canvas",
51562 fillOpacity: "canvas",
51563
51564 lineWidth: "canvas",
51565 lineCap: "canvas",
51566 lineJoin: "canvas",
51567 lineDash: "canvas",
51568 lineDashOffset: "canvas",
51569 miterLimit: "canvas",
51570
51571 shadowColor: "canvas",
51572 shadowOffsetX: "canvas",
51573 shadowOffsetY: "canvas",
51574 shadowBlur: "canvas",
51575
51576 translationX: "transform",
51577 translationY: "transform",
51578 rotationRads: "transform",
51579 rotationCenterX: "transform",
51580 rotationCenterY: "transform",
51581 scalingX: "transform",
51582 scalingY: "transform",
51583 scalingCenterX: "transform",
51584 scalingCenterY: "transform",
51585
51586 constrainGradients: "canvas"
51587 },
51588
51589 updaters: {
51590 "bbox": function (attrs) {
51591 attrs.bbox.plain.dirty = true;
51592 attrs.bbox.transform.dirty = true;
51593 if (
51594 attrs.rotationRads !== 0 && (attrs.rotationCenterX === null || attrs.rotationCenterY === null) ||
51595 ((attrs.scalingX !== 1 || attrs.scalingY !== 1) &&
51596 (attrs.scalingCenterX === null || attrs.scalingCenterY === null)
51597 )
51598 ) {
51599 if (!attrs.dirtyFlags.transform) {
51600 attrs.dirtyFlags.transform = [];
51601 }
51602 }
51603 },
51604
51605 "zIndex": function (attrs) {
51606 attrs.dirtyZIndex = true;
51607 },
51608
51609 "transform": function (attrs) {
51610 attrs.dirtyTransform = true;
51611 attrs.bbox.transform.dirty = true;
51612 }
51613 }
51614 }
51615 },
51616
51617 /**
51618 * @property {Object} attr
51619 * The visual attributes of the sprite, e.g. strokeStyle, fillStyle, lineWidth...
51620 */
51621 attr: {},
51622
51623 config: {
51624 parent: null
51625 },
51626
51627 onClassExtended: function (Class, member) {
51628 var initCfg = Class.superclass.self.def.initialConfig,
51629 cfg;
51630
51631 if (member.inheritableStatics && member.inheritableStatics.def) {
51632 cfg = Ext.merge({}, initCfg, member.inheritableStatics.def);
51633 Class.def = Ext.create("Ext.draw.sprite.AttributeDefinition", cfg);
51634 delete member.inheritableStatics.def;
51635 } else {
51636 Class.def = Ext.create("Ext.draw.sprite.AttributeDefinition", initCfg);
51637 }
51638 },
51639
51640 constructor: function (config) {
51641 if (this.$className === 'Ext.draw.sprite.Sprite') {
51642 throw 'Ext.draw.sprite.Sprite is an abstract class';
51643 }
51644 config = config || {};
51645 var me = this;
51646
51647 me.id = config.id || Ext.id(null, 'ext-sprite-');
51648 me.attr = {};
51649 me.initConfig(config);
51650 var modifiers = Ext.Array.from(config.modifiers, true);
51651 me.prepareModifiers(modifiers);
51652 me.initializeAttributes();
51653 me.setAttributes(me.self.def.getDefaults(), true);
51654 me.setAttributes(config);
51655 },
51656
51657 getDirty: function () {
51658 return this.attr.dirty;
51659 },
51660
51661 setDirty: function (dirty) {
51662 if ((this.attr.dirty = dirty)) {
51663 if (this._parent) {
51664 this._parent.setDirty(true);
51665 }
51666 }
51667 },
51668
51669 addModifier: function (modifier, reinitializeAttributes) {
51670 var me = this;
51671 if (!(modifier instanceof Ext.draw.modifier.Modifier)) {
51672 modifier = Ext.factory(modifier, null, null, 'modifier');
51673 }
51674 modifier.setSprite(this);
51675 if (modifier.preFx || modifier.config && modifier.config.preFx) {
51676 if (me.fx.getPrevious()) {
51677 me.fx.getPrevious().setNext(modifier);
51678 }
51679 modifier.setNext(me.fx);
51680 } else {
51681 me.topModifier.getPrevious().setNext(modifier);
51682 modifier.setNext(me.topModifier);
51683 }
51684 if (reinitializeAttributes) {
51685 me.initializeAttributes();
51686 }
51687 return modifier;
51688 },
51689
51690 prepareModifiers: function (additionalModifiers) {
51691 // Set defaults
51692 var me = this,
51693 modifier, i, ln;
51694
51695 me.topModifier = new Ext.draw.modifier.Target({sprite: me});
51696
51697 // Link modifiers
51698 me.fx = new Ext.draw.modifier.Animation({sprite: me});
51699 me.fx.setNext(me.topModifier);
51700
51701 for (i = 0, ln = additionalModifiers.length; i < ln; i++) {
51702 me.addModifier(additionalModifiers[i], false);
51703 }
51704 },
51705
51706 initializeAttributes: function () {
51707 var me = this;
51708 me.topModifier.prepareAttributes(me.attr);
51709 },
51710
51711 updateDirtyFlags: function (attrs) {
51712 var me = this,
51713 dirtyFlags = attrs.dirtyFlags, flags,
51714 updaters = me.self.def._updaters,
51715 any = false,
51716 dirty = false,
51717 flag;
51718
51719 do {
51720 any = false;
51721 for (flag in dirtyFlags) {
51722 me.updateDirtyFlags = Ext.emptyFn;
51723 flags = dirtyFlags[flag];
51724 delete dirtyFlags[flag];
51725 if (updaters[flag]) {
51726 updaters[flag].call(me, attrs, flags);
51727 }
51728 any = true;
51729 delete me.updateDirtyFlags;
51730 }
51731 dirty = dirty || any;
51732 } while (any);
51733
51734 if (dirty) {
51735 me.setDirty(true);
51736 }
51737 },
51738
51739 /**
51740 * Set attributes of the sprite.
51741 *
51742 * @param {Object} changes The content of the change.
51743 * @param {Boolean} [bypassNormalization] `true` to avoid normalization of the given changes.
51744 * @param {Boolean} [avoidCopy] `true` to avoid copying the `changes` object.
51745 * The content of object may be destroyed.
51746 */
51747 setAttributes: function (changes, bypassNormalization, avoidCopy) {
51748 var attributes = this.attr;
51749 if (bypassNormalization) {
51750 if (avoidCopy) {
51751 this.topModifier.pushDown(attributes, changes);
51752 } else {
51753 this.topModifier.pushDown(attributes, Ext.apply({}, changes));
51754 }
51755 } else {
51756 this.topModifier.pushDown(attributes, this.self.def.normalize(changes));
51757 }
51758 },
51759
51760 /**
51761 * Set attributes of the sprite, assuming the names and values have already been
51762 * normalized.
51763 *
51764 * @deprecated Use setAttributes directy with bypassNormalization argument being `true`.
51765 * @param {Object} changes The content of the change.
51766 * @param {Boolean} [avoidCopy] `true` to avoid copying the `changes` object.
51767 * The content of object may be destroyed.
51768 */
51769 setAttributesBypassingNormalization: function (changes, avoidCopy) {
51770 return this.setAttributes(changes, true, avoidCopy);
51771 },
51772
51773 /**
51774 * Returns the bounding box for the given Sprite as calculated with the Canvas engine.
51775 *
51776 * @param {Boolean} [isWithoutTransform] Whether to calculate the bounding box with the current transforms or not.
51777 */
51778 getBBox: function (isWithoutTransform) {
51779 var me = this,
51780 attr = me.attr,
51781 bbox = attr.bbox,
51782 plain = bbox.plain,
51783 transform = bbox.transform;
51784 if (plain.dirty) {
51785 me.updatePlainBBox(plain);
51786 plain.dirty = false;
51787 }
51788 if (isWithoutTransform) {
51789 return plain;
51790 } else {
51791 me.applyTransformations();
51792 if (transform.dirty) {
51793 me.updateTransformedBBox(transform, plain);
51794 transform.dirty = false;
51795 }
51796 return transform;
51797 }
51798 },
51799
51800 /**
51801 * @protected
51802 * Subclass will fill the plain object with `x`, `y`, `width`, `height` information of the plain bounding box of
51803 * this sprite.
51804 *
51805 * @param {Object} plain Target object.
51806 */
51807 updatePlainBBox: Ext.emptyFn,
51808
51809 /**
51810 * @protected
51811 * Subclass will fill the plain object with `x`, `y`, `width`, `height` information of the transformed
51812 * bounding box of this sprite.
51813 *
51814 * @param {Object} transform Target object.
51815 * @param {Object} plain Auxiliary object providing information of plain object.
51816 */
51817 updateTransformedBBox: function (transform, plain) {
51818 this.attr.matrix.transformBBox(plain, 0, transform);
51819 },
51820
51821 /**
51822 * Subclass can rewrite this function to gain better performance.
51823 * @param {Boolean} isWithoutTransform
51824 * @return {Array}
51825 */
51826 getBBoxCenter: function (isWithoutTransform) {
51827 var bbox = this.getBBox(isWithoutTransform);
51828 if (bbox) {
51829 return [
51830 bbox.x + bbox.width * 0.5,
51831 bbox.y + bbox.height * 0.5
51832 ];
51833 } else {
51834 return [0, 0];
51835 }
51836 },
51837
51838 /**
51839 * Hide the sprite.
51840 * @return {Ext.draw.sprite.Sprite} this
51841 * @chainable
51842 */
51843 hide: function () {
51844 this.attr.hidden = true;
51845 this.setDirty(true);
51846 return this;
51847 },
51848
51849 /**
51850 * Show the sprite.
51851 * @return {Ext.draw.sprite.Sprite} this
51852 * @chainable
51853 */
51854 show: function () {
51855 this.attr.hidden = false;
51856 this.setDirty(true);
51857 return this;
51858 },
51859
51860 /**
51861 * Applies sprite's attributes to the given context.
51862 * @param {Object} ctx Context to apply sprite's attributes to.
51863 * @param {Array} region The region of the context to be affected by gradients.
51864 */
51865 useAttributes: function (ctx, region) {
51866 this.applyTransformations();
51867 var attrs = this.attr,
51868 canvasAttributes = attrs.canvasAttributes,
51869 strokeStyle = canvasAttributes.strokeStyle,
51870 fillStyle = canvasAttributes.fillStyle,
51871 lineDash = canvasAttributes.lineDash,
51872 lineDashOffset = canvasAttributes.lineDashOffset,
51873 id;
51874
51875 if (strokeStyle) {
51876 if (strokeStyle.isGradient) {
51877 ctx.strokeStyle = 'black';
51878 ctx.strokeGradient = strokeStyle;
51879 } else {
51880 ctx.strokeGradient = false;
51881 }
51882 }
51883
51884 if (fillStyle) {
51885 if (fillStyle.isGradient) {
51886 ctx.fillStyle = 'black';
51887 ctx.fillGradient = fillStyle;
51888 } else {
51889 ctx.fillGradient = false;
51890 }
51891 }
51892
51893 if (lineDash && ctx.setLineDash) {
51894 ctx.setLineDash(lineDash);
51895 }
51896
51897 if (lineDashOffset && typeof ctx.lineDashOffset === 'number') {
51898 ctx.lineDashOffset = lineDashOffset;
51899 }
51900
51901 for (id in canvasAttributes) {
51902 if (canvasAttributes[id] !== undefined && canvasAttributes[id] !== ctx[id]) {
51903 ctx[id] = canvasAttributes[id];
51904 }
51905 }
51906
51907 if(attrs.constrainGradients) {
51908 ctx.setGradientBBox({x: region[0], y: region[1], width: region[2], height: region[3]});
51909 } else {
51910 ctx.setGradientBBox(this.getBBox(attrs.transformFillStroke));
51911 }
51912 },
51913
51914 /**
51915 * @private
51916 *
51917 * Calculates forward and inverse transform matrices.
51918 * @param {Boolean} force Forces recalculation of transform matrices even when sprite's transform attributes supposedly haven't changed.
51919 */
51920 applyTransformations: function (force) {
51921 if (!force && !this.attr.dirtyTransform) {
51922 return;
51923 }
51924 var me = this,
51925 attr = me.attr,
51926 center = me.getBBoxCenter(true),
51927 centerX = center[0],
51928 centerY = center[1],
51929
51930 x = attr.translationX,
51931 y = attr.translationY,
51932
51933 sx = attr.scalingX,
51934 sy = attr.scalingY === null ? attr.scalingX : attr.scalingY,
51935 scx = attr.scalingCenterX === null ? centerX : attr.scalingCenterX,
51936 scy = attr.scalingCenterY === null ? centerY : attr.scalingCenterY,
51937
51938 rad = attr.rotationRads,
51939 rcx = attr.rotationCenterX === null ? centerX : attr.rotationCenterX,
51940 rcy = attr.rotationCenterY === null ? centerY : attr.rotationCenterY,
51941
51942 cos = Math.cos(rad),
51943 sin = Math.sin(rad);
51944
51945 if (sx === 1 && sy === 1) {
51946 scx = 0;
51947 scy = 0;
51948 }
51949
51950 if (rad === 0) {
51951 rcx = 0;
51952 rcy = 0;
51953 }
51954
51955 attr.matrix.elements = [
51956 cos * sx, sin * sy,
51957 -sin * sx, cos * sy,
51958 scx + (rcx - cos * rcx - scx + rcy * sin) * sx + x,
51959 scy + (rcy - cos * rcy - scy + rcx * -sin) * sy + y
51960 ];
51961 attr.matrix.inverse(attr.inverseMatrix);
51962 attr.dirtyTransform = false;
51963 attr.bbox.transform.dirty = true;
51964 },
51965
51966 /**
51967 * Called before rendering.
51968 */
51969 preRender: Ext.emptyFn,
51970
51971 /**
51972 * Render method.
51973 * @param {Ext.draw.Surface} surface The surface.
51974 * @param {Object} ctx A context object compatible with CanvasRenderingContext2D.
51975 * @param {Array} region The clip region (or called dirty rect) of the current rendering. Not be confused
51976 * with `surface.getRegion()`.
51977 *
51978 * @return {*} returns `false` to stop rendering in this frame. All the sprite haven't been rendered
51979 * will have their dirty flag untouched.
51980 */
51981 render: Ext.emptyFn,
51982
51983 repaint: function () {
51984 var parent = this.getParent();
51985 while (parent && !(parent instanceof Ext.draw.Surface)) {
51986 parent = parent.getParent();
51987 }
51988 if (parent) {
51989 parent.renderFrame();
51990 }
51991 },
51992
51993 /**
51994 * Removes the sprite and clears all listeners.
51995 */
51996 destroy: function () {
51997 var me = this, modifier = me.topModifier, curr;
51998 while (modifier) {
51999 curr = modifier;
52000 modifier = modifier.getPrevious();
52001 curr.destroy();
52002 }
52003 delete me.attr;
52004
52005 me.destroy = Ext.emptyFn;
52006 if (me.fireEvent('beforedestroy', me) !== false) {
52007 me.fireEvent('destroy', me);
52008 }
52009 this.callSuper();
52010 }
52011 }, function () {
52012 this.def = Ext.create("Ext.draw.sprite.AttributeDefinition", this.def);
52013 });
52014
52015
52016 Ext.define('Ext.draw.sprite.Line', {
52017 extend: Ext.draw.sprite.Sprite ,
52018 alias: 'sprite.line',
52019 type: 'line',
52020
52021 inheritableStatics: {
52022 def: {
52023 processors: {
52024 fromX: 'number',
52025 fromY: 'number',
52026 toX: 'number',
52027 toY: 'number'
52028 },
52029
52030 defaults: {
52031 fromX: 0,
52032 fromY: 0,
52033 toX: 1,
52034 toY: 1
52035 }
52036 }
52037 },
52038
52039 render: function (surface, ctx, clipRegion) {
52040 var attr = this.attr,
52041 matrix = this.attr.matrix;
52042
52043 matrix.toContext(ctx);
52044
52045 ctx.beginPath();
52046 ctx.moveTo(attr.fromX, attr.fromY);
52047 ctx.lineTo(attr.toX, attr.toY);
52048 ctx.stroke();
52049 }
52050 });
52051
52052 (function () {
52053 var PI2_3 = 2.0943951023931953/* 120 Deg */,
52054 abs = Math.abs,
52055 sin = Math.cos,
52056 cos = Math.cos,
52057 acos = Math.acos,
52058 sqrt = Math.sqrt,
52059 exp = Math.exp,
52060 log = Math.log;
52061
52062 /**
52063 * @private
52064 * Singleton Class that provides methods to solve cubic equation.
52065 */
52066 Ext.define("Ext.draw.Solver", {
52067 singleton: true,
52068 /**
52069 * Cubic root of number
52070 * @param {Number} number
52071 */
52072 cubicRoot: function (number) {
52073 if (number > 0) {
52074 return exp(log(number) / 3);
52075 } else if (number < 0) {
52076 return -exp(log(-number) / 3);
52077 } else {
52078 return 0;
52079 }
52080 },
52081
52082 /**
52083 * Returns the function f(x) = a * x + b and solver for f(x) = y
52084 * @param {Number} a
52085 * @param {Number} b
52086 */
52087 linearFunction: function (a, b) {
52088 var result;
52089 if (a === 0) {
52090 result = function (t) {
52091 return b;
52092 };
52093 result.solve = function (y) {
52094 // if y == d there should be a real root
52095 // but we can ignore it for geometry calculations.
52096 return [];
52097 };
52098 } else {
52099 result = function (t) {
52100 return a * t + b;
52101 };
52102 result.solve = function (y) {
52103 return [(y - b) / a];
52104 };
52105 }
52106 return result;
52107 },
52108
52109 /**
52110 * Returns the function f(x) = a * x ^ 2 + b * x + c and solver for f(x) = y
52111 *
52112 * @param {Number} a
52113 * @param {Number} b
52114 * @param {Number} c
52115 */
52116 quadraticFunction: function (a, b, c) {
52117 var result;
52118 if (a === 0) {
52119 return this.linearFunction(b, c);
52120 } else {
52121 // Quadratic equation.
52122 result = function (t) {
52123 return (a * t + b) * t + c;
52124 };
52125 var delta0temp = b * b - 4 * a * c,
52126 delta = function (y) {
52127 return delta0temp + 4 * a * y;
52128 }, solveTemp0 = 1 / a * 0.5,
52129 solveTemp1 = -solveTemp0 * b;
52130 solveTemp0 = abs(solveTemp0);
52131 result.solve = function (y) {
52132 var deltaTemp = delta(y);
52133 if (deltaTemp < 0) {
52134 return [];
52135 }
52136 deltaTemp = sqrt(deltaTemp);
52137 // have to distinct roots here.
52138 return [solveTemp1 - deltaTemp * solveTemp0, solveTemp1 + deltaTemp * solveTemp0];
52139 };
52140 }
52141 return result;
52142 },
52143
52144 /**
52145 * Returns the function f(x) = a * x^3 + b * x^2 + c * x + d and solver for f(x) = y
52146 * @param {Number} a
52147 * @param {Number} b
52148 * @param {Number} c
52149 * @param {Number} d
52150 */
52151 cubicFunction: function (a, b, c, d) {
52152 var result;
52153 if (a === 0) {
52154 return this.quadraticFunction(b, c, d);
52155 } else {
52156 result = function (t) {
52157 return ((a * t + b) * t + c) * t + d;
52158 };
52159
52160 var b_a_3 = b / a / 3,
52161 c_a = c / a,
52162 d_a = d / a,
52163 b2 = b_a_3 * b_a_3,
52164 deltaTemp0 = (b_a_3 * c_a - d_a) * 0.5 - b_a_3 * b2,
52165 deltaTemp1 = b2 - c_a / 3,
52166 deltaTemp13 = deltaTemp1 * deltaTemp1 * deltaTemp1;
52167
52168 if (deltaTemp1 === 0) {
52169 result.solve = function (y) {
52170 return [-b_a_3 + this.cubicRoot(deltaTemp0 * 2 + y / a)];
52171 };
52172 } else {
52173 if (deltaTemp1 > 0) {
52174 var deltaTemp1_2 = sqrt(deltaTemp1),
52175 deltaTemp13_2 = deltaTemp1_2 * deltaTemp1_2 * deltaTemp1_2;
52176 deltaTemp1_2 += deltaTemp1_2;
52177 }
52178 result.solve = function (y) {
52179 y /= a;
52180 var d0 = deltaTemp0 + y * 0.5,
52181 deltaTemp = d0 * d0 - deltaTemp13;
52182 if (deltaTemp > 0) {
52183 deltaTemp = sqrt(deltaTemp);
52184 return [-b_a_3 + this.cubicRoot(d0 + deltaTemp) + this.cubicRoot(d0 - deltaTemp)];
52185 } else if (deltaTemp === 0) {
52186 var cr = this.cubicRoot(d0),
52187 root0 = -b_a_3 - cr;
52188 if (d0 >= 0) {
52189 return [root0, root0, -b_a_3 + 2 * cr];
52190 } else {
52191 return [-b_a_3 + 2 * cr, root0, root0];
52192 }
52193 } else {
52194 var theta = acos(d0 / deltaTemp13_2) / 3,
52195 ra = deltaTemp1_2 * cos(theta) - b_a_3,
52196 rb = deltaTemp1_2 * cos(theta + PI2_3) - b_a_3,
52197 rc = deltaTemp1_2 * cos(theta - PI2_3) - b_a_3;
52198 if (ra < rb) {
52199 if (rb < rc) {
52200 return [ra, rb, rc];
52201 } else if (ra < rc) {
52202 return[ra, rc, rb];
52203 } else {
52204 return [rc, ra, rb];
52205 }
52206 } else {
52207 if (ra < rc) {
52208 return [rb, ra, rc];
52209 } else if (rb < rc) {
52210 return [rb, rc, ra];
52211 } else {
52212 return [rc, rb, ra];
52213 }
52214 }
52215 }
52216 };
52217 }
52218 }
52219 return result;
52220 },
52221
52222 createBezierSolver: function (a, b, c, d) {
52223 return this.cubicFunction(3 * (b - c) + d - a, 3 * (a - 2 * b + c), 3 * (b - a), a);
52224 }
52225 });
52226 })();
52227
52228 /**
52229 * Class representing a path.
52230 * Designed to be compatible with [CanvasPathMethods](http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#canvaspathmethods)
52231 * and will hopefully be replaced by the browsers' implementation of the Path object.
52232 */
52233 Ext.define('Ext.draw.Path', {
52234
52235 statics: {
52236 pathRe: /,?([achlmqrstvxz]),?/gi,
52237 pathRe2: /-/gi,
52238 pathSplitRe: /\s|,/g
52239 },
52240 svgString: '',
52241
52242 /**
52243 * Create a path from pathString
52244 * @constructor
52245 * @param {String} pathString
52246 */
52247 constructor: function (pathString) {
52248 var me = this;
52249 me.coords = [];
52250 me.types = [];
52251 me.cursor = null;
52252 me.startX = 0;
52253 me.startY = 0;
52254 me.solvers = {};
52255 if (pathString) {
52256 me.fromSvgString(pathString);
52257 }
52258 },
52259
52260 /**
52261 * Clear the path.
52262 */
52263 clear: function () {
52264 var me = this;
52265 me.coords.length = 0;
52266 me.types.length = 0;
52267 me.cursor = null;
52268 me.startX = 0;
52269 me.startY = 0;
52270 me.solvers = {};
52271 me.dirt();
52272 },
52273
52274 /**
52275 * @private
52276 */
52277 dirt: function () {
52278 this.svgString = '';
52279 },
52280
52281 /**
52282 * Move to a position.
52283 * @param {Number} x
52284 * @param {Number} y
52285 */
52286 moveTo: function (x, y) {
52287 var me = this;
52288 if (!me.cursor) {
52289 me.cursor = [x, y];
52290 }
52291 me.coords.push(x, y);
52292 me.types.push('M');
52293 me.startX = x;
52294 me.startY = y;
52295 me.cursor[0] = x;
52296 me.cursor[1] = y;
52297 me.dirt();
52298 },
52299
52300 /**
52301 * A straight line to a position.
52302 * @param {Number} x
52303 * @param {Number} y
52304 */
52305 lineTo: function (x, y) {
52306 var me = this;
52307 if (!me.cursor) {
52308 me.cursor = [x, y];
52309 me.coords.push(x, y);
52310 me.types.push('M');
52311 } else {
52312 me.coords.push(x, y);
52313 me.types.push('L');
52314 }
52315 me.cursor[0] = x;
52316 me.cursor[1] = y;
52317 me.dirt();
52318 },
52319
52320 /**
52321 * A cubic bezier curve to a position.
52322 * @param {Number} cx1
52323 * @param {Number} cy1
52324 * @param {Number} cx2
52325 * @param {Number} cy2
52326 * @param {Number} x
52327 * @param {Number} y
52328 */
52329 bezierCurveTo: function (cx1, cy1, cx2, cy2, x, y) {
52330 var me = this;
52331 if (!me.cursor) {
52332 me.moveTo(cx1, cy1);
52333 }
52334 me.coords.push(cx1, cy1, cx2, cy2, x, y);
52335 me.types.push('C');
52336 me.cursor[0] = x;
52337 me.cursor[1] = y;
52338 me.dirt();
52339 },
52340
52341 /**
52342 * A quadratic bezier curve to a position.
52343 * @param {Number} cx
52344 * @param {Number} cy
52345 * @param {Number} x
52346 * @param {Number} y
52347 */
52348 quadraticCurveTo: function (cx, cy, x, y) {
52349 var me = this;
52350 if (!me.cursor) {
52351 me.moveTo(cx, cy);
52352 }
52353 me.bezierCurveTo(
52354 (me.cursor[0] * 2 + cx) / 3, (me.cursor[1] * 2 + cy) / 3,
52355 (x * 2 + cx) / 3, (y * 2 + cy) / 3,
52356 x, y
52357 );
52358 },
52359
52360 /**
52361 * Close this path with a straight line.
52362 */
52363 closePath: function () {
52364 var me = this;
52365 if (me.cursor) {
52366 me.types.push('Z');
52367 me.dirt();
52368 }
52369 },
52370
52371 /**
52372 * Create a elliptic arc curve compatible with SVG's arc to instruction.
52373 *
52374 * The curve start from (`x1`, `y1`) and ends at (`x2`, `y2`). The ellipse
52375 * has radius `rx` and `ry` and a rotation of `rotation`.
52376 * @param {Number} x1
52377 * @param {Number} y1
52378 * @param {Number} x2
52379 * @param {Number} y2
52380 * @param {Number} [rx]
52381 * @param {Number} [ry]
52382 * @param {Number} [rotation]
52383 */
52384 arcTo: function (x1, y1, x2, y2, rx, ry, rotation) {
52385 var me = this;
52386 if (ry === undefined) {
52387 ry = rx;
52388 }
52389
52390 if (rotation === undefined) {
52391 rotation = 0;
52392 }
52393
52394 if (!me.cursor) {
52395 me.moveTo(x1, y1);
52396 return;
52397 }
52398
52399 if (rx === 0 || ry === 0) {
52400 me.lineTo(x1, y1);
52401 return;
52402 }
52403
52404 x2 -= x1;
52405 y2 -= y1;
52406
52407 var x0 = me.cursor[0] - x1,
52408 y0 = me.cursor[1] - y1,
52409 area = x2 * y0 - y2 * x0,
52410 cos, sin, xx, yx, xy, yy,
52411 l0 = Math.sqrt(x0 * x0 + y0 * y0),
52412 l2 = Math.sqrt(x2 * x2 + y2 * y2),
52413 dist, cx, cy;
52414 // cos rx, -sin ry , x1 - cos rx x1 + ry sin y1
52415 // sin rx, cos ry, -rx sin x1 + y1 - cos ry y1
52416 if (area === 0) {
52417 me.lineTo(x1, y1);
52418 return;
52419 }
52420
52421 if (ry !== rx) {
52422 cos = Math.cos(rotation);
52423 sin = Math.sin(rotation);
52424 xx = cos / rx;
52425 yx = sin / ry;
52426 xy = -sin / rx;
52427 yy = cos / ry;
52428 var temp = xx * x0 + yx * y0;
52429 y0 = xy * x0 + yy * y0;
52430 x0 = temp;
52431 temp = xx * x2 + yx * y2;
52432 y2 = xy * x2 + yy * y2;
52433 x2 = temp;
52434 } else {
52435 x0 /= rx;
52436 y0 /= ry;
52437 x2 /= rx;
52438 y2 /= ry;
52439 }
52440
52441 cx = x0 * l2 + x2 * l0;
52442 cy = y0 * l2 + y2 * l0;
52443 dist = 1 / (Math.sin(Math.asin(Math.abs(area) / (l0 * l2)) * 0.5) * Math.sqrt(cx * cx + cy * cy));
52444 cx *= dist;
52445 cy *= dist;
52446
52447 var k0 = (cx * x0 + cy * y0) / (x0 * x0 + y0 * y0),
52448 k2 = (cx * x2 + cy * y2) / (x2 * x2 + y2 * y2);
52449 var cosStart = x0 * k0 - cx,
52450 sinStart = y0 * k0 - cy,
52451 cosEnd = x2 * k2 - cx,
52452 sinEnd = y2 * k2 - cy,
52453 startAngle = Math.atan2(sinStart, cosStart),
52454 endAngle = Math.atan2(sinEnd, cosEnd);
52455 if (area > 0) {
52456 if (endAngle < startAngle) {
52457 endAngle += Math.PI * 2;
52458 }
52459 } else {
52460 if (startAngle < endAngle) {
52461 startAngle += Math.PI * 2;
52462 }
52463 }
52464 if (ry !== rx) {
52465 cx = cos * cx * rx - sin * cy * ry + x1;
52466 cy = sin * cy * ry + cos * cy * ry + y1;
52467 me.lineTo(cos * rx * cosStart - sin * ry * sinStart + cx,
52468 sin * rx * cosStart + cos * ry * sinStart + cy);
52469 me.ellipse(cx, cy, rx, ry, rotation, startAngle, endAngle, area < 0);
52470 } else {
52471 cx = cx * rx + x1;
52472 cy = cy * ry + y1;
52473 me.lineTo(rx * cosStart + cx, ry * sinStart + cy);
52474 me.ellipse(cx, cy, rx, ry, rotation, startAngle, endAngle, area < 0);
52475 }
52476 },
52477
52478 /**
52479 * Create an elliptic arc.
52480 *
52481 * See [the whatwg reference of ellipse](http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#dom-context-2d-ellipse).
52482 *
52483 * @param {Number} cx
52484 * @param {Number} cy
52485 * @param {Number} radiusX
52486 * @param {Number} radiusY
52487 * @param {Number} rotation
52488 * @param {Number} startAngle
52489 * @param {Number} endAngle
52490 * @param {Number} anticlockwise
52491 */
52492 ellipse: function (cx, cy, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise) {
52493 var me = this,
52494 coords = me.coords,
52495 start = coords.length, count,
52496 i, j;
52497 if (endAngle - startAngle >= Math.PI * 2) {
52498 me.ellipse(cx, cy, radiusX, radiusY, rotation, startAngle, startAngle + Math.PI, anticlockwise);
52499 me.ellipse(cx, cy, radiusX, radiusY, rotation, startAngle + Math.PI, endAngle, anticlockwise);
52500 return;
52501 }
52502 if (!anticlockwise) {
52503 if (endAngle < startAngle) {
52504 endAngle += Math.PI * 2;
52505 }
52506 count = me.approximateArc(coords, cx, cy, radiusX, radiusY, rotation, startAngle, endAngle);
52507 } else {
52508 if (startAngle < endAngle) {
52509 startAngle += Math.PI * 2;
52510 }
52511 count = me.approximateArc(coords, cx, cy, radiusX, radiusY, rotation, endAngle, startAngle);
52512 for (i = start, j = coords.length - 2; i < j; i += 2, j -= 2) {
52513 var temp = coords[i];
52514 coords[i] = coords[j];
52515 coords[j] = temp;
52516 temp = coords[i + 1];
52517 coords[i + 1] = coords[j + 1];
52518 coords[j + 1] = temp;
52519 }
52520 }
52521
52522 if (!me.cursor) {
52523 me.cursor = [coords[coords.length - 2], coords[coords.length - 1]];
52524 me.types.push('M');
52525 } else {
52526 me.cursor[0] = coords[coords.length - 2];
52527 me.cursor[1] = coords[coords.length - 1];
52528 me.types.push('L');
52529 }
52530
52531 for (i = 2; i < count; i += 6) {
52532 me.types.push('C');
52533 }
52534 me.dirt();
52535 },
52536
52537 /**
52538 * Create an circular arc.
52539 *
52540 * @param {Number} x
52541 * @param {Number} y
52542 * @param {Number} radius
52543 * @param {Number} startAngle
52544 * @param {Number} endAngle
52545 * @param {Number} anticlockwise
52546 */
52547 arc: function (x, y, radius, startAngle, endAngle, anticlockwise) {
52548 this.ellipse(x, y, radius, radius, 0, startAngle, endAngle, anticlockwise);
52549 },
52550
52551 /**
52552 * Draw a rectangle and close it.
52553 *
52554 * @param {Number} x
52555 * @param {Number} y
52556 * @param {Number} width
52557 * @param {Number} height
52558 */
52559 rect: function (x, y, width, height) {
52560 if (width == 0 || height == 0) {
52561 return;
52562 }
52563 var me = this;
52564 me.moveTo(x, y);
52565 me.lineTo(x + width, y);
52566 me.lineTo(x + width, y + height);
52567 me.lineTo(x, y + height);
52568 me.closePath();
52569 },
52570
52571 /**
52572 * @private
52573 * @param {Array} result
52574 * @param {Number} cx
52575 * @param {Number} cy
52576 * @param {Number} rx
52577 * @param {Number} ry
52578 * @param {Number} phi
52579 * @param {Number} theta1
52580 * @param {Number} theta2
52581 * @return {Number}
52582 */
52583 approximateArc: function (result, cx, cy, rx, ry, phi, theta1, theta2) {
52584 var cosPhi = Math.cos(phi),
52585 sinPhi = Math.sin(phi),
52586 cosTheta1 = Math.cos(theta1),
52587 sinTheta1 = Math.sin(theta1),
52588 xx = cosPhi * cosTheta1 * rx - sinPhi * sinTheta1 * ry,
52589 yx = -cosPhi * sinTheta1 * rx - sinPhi * cosTheta1 * ry,
52590 xy = sinPhi * cosTheta1 * rx + cosPhi * sinTheta1 * ry,
52591 yy = -sinPhi * sinTheta1 * rx + cosPhi * cosTheta1 * ry,
52592 rightAngle = Math.PI / 2,
52593 count = 2,
52594 exx = xx,
52595 eyx = yx,
52596 exy = xy,
52597 eyy = yy,
52598 rho = 0.547443256150549,
52599 temp, y1, x3, y3, x2, y2;
52600
52601 theta2 -= theta1;
52602 if (theta2 < 0) {
52603 theta2 += Math.PI * 2;
52604 }
52605 result.push(xx + cx, xy + cy);
52606 while (theta2 >= rightAngle) {
52607 result.push(
52608 exx + eyx * rho + cx, exy + eyy * rho + cy,
52609 exx * rho + eyx + cx, exy * rho + eyy + cy,
52610 eyx + cx, eyy + cy
52611 );
52612 count += 6;
52613 theta2 -= rightAngle;
52614 temp = exx;
52615 exx = eyx;
52616 eyx = -temp;
52617 temp = exy;
52618 exy = eyy;
52619 eyy = -temp;
52620 }
52621 if (theta2) {
52622 y1 = (0.3294738052815987 + 0.012120855841304373 * theta2) * theta2;
52623 x3 = Math.cos(theta2);
52624 y3 = Math.sin(theta2);
52625 x2 = x3 + y1 * y3;
52626 y2 = y3 - y1 * x3;
52627 result.push(
52628 exx + eyx * y1 + cx, exy + eyy * y1 + cy,
52629 exx * x2 + eyx * y2 + cx, exy * x2 + eyy * y2 + cy,
52630 exx * x3 + eyx * y3 + cx, exy * x3 + eyy * y3 + cy
52631 );
52632 count += 6;
52633 }
52634 return count;
52635 },
52636
52637 /**
52638 * [http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes](http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes)
52639 * @param {Number} rx
52640 * @param {Number} ry
52641 * @param {Number} rotation Differ from svg spec, this is radian.
52642 * @param {Number} fA
52643 * @param {Number} fS
52644 * @param {Number} x2
52645 * @param {Number} y2
52646 */
52647 arcSvg: function (rx, ry, rotation, fA, fS, x2, y2) {
52648 if (rx < 0) {
52649 rx = -rx;
52650 }
52651 if (ry < 0) {
52652 ry = -ry;
52653 }
52654 var me = this,
52655 x1 = me.cursor[0],
52656 y1 = me.cursor[1],
52657 hdx = (x1 - x2) / 2,
52658 hdy = (y1 - y2) / 2,
52659 cosPhi = Math.cos(rotation),
52660 sinPhi = Math.sin(rotation),
52661 xp = hdx * cosPhi + hdy * sinPhi,
52662 yp = -hdx * sinPhi + hdy * cosPhi,
52663 ratX = xp / rx,
52664 ratY = yp / ry,
52665 lambda = ratX * ratX + ratY * ratY,
52666 cx = (x1 + x2) * 0.5, cy = (y1 + y2) * 0.5,
52667 cpx = 0, cpy = 0;
52668 if (lambda >= 1) {
52669 lambda = Math.sqrt(lambda);
52670 rx *= lambda;
52671 ry *= lambda;
52672 // me gives lambda == cpx == cpy == 0;
52673 } else {
52674 lambda = Math.sqrt(1 / lambda - 1);
52675 if (fA === fS) {
52676 lambda = -lambda;
52677 }
52678 cpx = lambda * rx * ratY;
52679 cpy = -lambda * ry * ratX;
52680 cx += cosPhi * cpx - sinPhi * cpy;
52681 cy += sinPhi * cpx + cosPhi * cpy;
52682 }
52683
52684 var theta1 = Math.atan2((yp - cpy) / ry, (xp - cpx) / rx),
52685 deltaTheta = Math.atan2((-yp - cpy) / ry, (-xp - cpx) / rx) - theta1;
52686
52687 if (fS) {
52688 if (deltaTheta <= 0) {
52689 deltaTheta += Math.PI * 2;
52690 }
52691 } else {
52692 if (deltaTheta >= 0) {
52693 deltaTheta -= Math.PI * 2;
52694 }
52695 }
52696 me.ellipse(cx, cy, rx, ry, rotation, theta1, theta1 + deltaTheta, 1 - fS);
52697 },
52698
52699 /**
52700 * Feed the path from svg path string.
52701 * @param {String} pathString
52702 */
52703 fromSvgString: function (pathString) {
52704 if (!pathString) {
52705 return;
52706 }
52707 var me = this,
52708 parts,
52709 paramCounts = {
52710 a: 7, c: 6, h: 1, l: 2, m: 2, q: 4, s: 4, t: 2, v: 1, z: 0,
52711 A: 7, C: 6, H: 1, L: 2, M: 2, Q: 4, S: 4, T: 2, V: 1, Z: 0
52712 },
52713 lastCommand = '',
52714 lastControlX, lastControlY,
52715 lastX = 0, lastY = 0,
52716 part = false, i, partLength, relative;
52717
52718 // Split the string to items.
52719 if (Ext.isString(pathString)) {
52720 parts = pathString.replace(Ext.draw.Path.pathRe, " $1 ").replace(Ext.draw.Path.pathRe2, " -").split(Ext.draw.Path.pathSplitRe);
52721 } else if (Ext.isArray(pathString)) {
52722 parts = pathString.join(',').split(Ext.draw.Path.pathSplitRe);
52723 }
52724
52725 // Remove empty entries
52726 for (i = 0, partLength = 0; i < parts.length; i++) {
52727 if (parts[i] !== '') {
52728 parts[partLength++] = parts[i];
52729 }
52730 }
52731 parts.length = partLength;
52732
52733 me.clear();
52734 for (i = 0; i < parts.length;) {
52735 lastCommand = part;
52736 part = parts[i];
52737 relative = (part.toUpperCase() !== part);
52738 i++;
52739 switch (part) {
52740 case 'M':
52741 me.moveTo(lastX = +parts[i], lastY = +parts[i + 1]);
52742 i += 2;
52743 while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
52744 me.lineTo(lastX = +parts[i], lastY = +parts[i + 1]);
52745 i += 2;
52746 }
52747 break;
52748 case 'L':
52749 me.lineTo(lastX = +parts[i], lastY = +parts[i + 1]);
52750 i += 2;
52751 while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
52752 me.lineTo(lastX = +parts[i], lastY = +parts[i + 1]);
52753 i += 2;
52754 }
52755 break;
52756 case 'A':
52757 while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
52758 me.arcSvg(
52759 +parts[i], +parts[i + 1],
52760 +parts[i + 2] * Math.PI / 180,
52761 +parts[i + 3], +parts[i + 4],
52762 lastX = +parts[i + 5], lastY = +parts[i + 6]);
52763 i += 7;
52764 }
52765 break;
52766 case 'C':
52767 while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
52768 me.bezierCurveTo(
52769 +parts[i ], +parts[i + 1],
52770 lastControlX = +parts[i + 2], lastControlY = +parts[i + 3],
52771 lastX = +parts[i + 4], lastY = +parts[i + 5]);
52772 i += 6;
52773 }
52774 break;
52775 case 'Z':
52776 me.closePath();
52777 break;
52778 case 'm':
52779 me.moveTo(lastX += +parts[i], lastY += +parts[i + 1]);
52780 i += 2;
52781 while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
52782 me.lineTo(lastX += +parts[i], lastY += +parts[i + 1]);
52783 i += 2;
52784 }
52785 break;
52786 case 'l':
52787 me.lineTo(lastX += +parts[i], lastY += +parts[i + 1]);
52788 i += 2;
52789 while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
52790 me.lineTo(lastX += +parts[i], lastY += +parts[i + 1]);
52791 i += 2;
52792 }
52793 break;
52794 case 'a':
52795 while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
52796 me.arcSvg(
52797 +parts[i], +parts[i + 1],
52798 +parts[i + 2] * Math.PI / 180,
52799 +parts[i + 3], +parts[i + 4],
52800 lastX += +parts[i + 5], lastY += +parts[i + 6]);
52801 i += 7;
52802 }
52803 break;
52804 case 'c':
52805 while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
52806 me.bezierCurveTo(lastX + (+parts[i]), lastY + (+parts[i + 1]),
52807 lastControlX = lastX + (+parts[i + 2]), lastControlY = lastY + (+parts[i + 3]),
52808 lastX += +parts[i + 4], lastY += +parts[i + 5]);
52809 i += 6;
52810 }
52811 break;
52812 case 'z':
52813 me.closePath();
52814 break;
52815 case 's':
52816 if (!(lastCommand === 'c' || lastCommand === 'C' || lastCommand === 's' || lastCommand === 'S')) {
52817 lastControlX = lastX;
52818 lastControlY = lastY;
52819 }
52820
52821 while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
52822 me.bezierCurveTo(
52823 lastX + lastX - lastControlX, lastY + lastY - lastControlY,
52824 lastControlX = lastX + (+parts[i]), lastControlY = lastY + (+parts[i + 1]),
52825 lastX += +parts[i + 2], lastY += +parts[i + 3]);
52826 i += 4;
52827 }
52828 break;
52829 case 'S':
52830 if (!(lastCommand === 'c' || lastCommand === 'C' || lastCommand === 's' || lastCommand === 'S')) {
52831 lastControlX = lastX;
52832 lastControlY = lastY;
52833 }
52834 while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
52835 me.bezierCurveTo(
52836 lastX + lastX - lastControlX, lastY + lastY - lastControlY,
52837 lastControlX = +parts[i], lastControlY = +parts[i + 1],
52838 lastX = (+parts[i + 2]), lastY = (+parts[i + 3]));
52839 i += 4;
52840 }
52841 break;
52842 case 'q':
52843 while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
52844 me.quadraticCurveTo(
52845 lastControlX = lastX + (+parts[i]), lastControlY = lastY + (+parts[i + 1]),
52846 lastX += +parts[i + 2], lastY += +parts[i + 3]);
52847 i += 4;
52848 }
52849 break;
52850 case 'Q':
52851 while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
52852 me.quadraticCurveTo(
52853 lastControlX = +parts[i], lastControlY = +parts[i + 1],
52854 lastX = +parts[i + 2], lastY = +parts[i + 3]);
52855 i += 4;
52856 }
52857 break;
52858 case 't':
52859 if (!(lastCommand === 'q' || lastCommand === 'Q' || lastCommand === 't' || lastCommand === 'T')) {
52860 lastControlX = lastX;
52861 lastControlY = lastY;
52862 }
52863 while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
52864 me.quadraticCurveTo(
52865 lastControlX = lastX + lastX - lastControlX, lastControlY = lastY + lastY - lastControlY,
52866 lastX += +parts[i + 1], lastY += +parts[i + 2]);
52867 i += 2;
52868 }
52869 break;
52870 case 'T':
52871 if (!(lastCommand === 'q' || lastCommand === 'Q' || lastCommand === 't' || lastCommand === 'T')) {
52872 lastControlX = lastX;
52873 lastControlY = lastY;
52874 }
52875 while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
52876 me.quadraticCurveTo(
52877 lastControlX = lastX + lastX - lastControlX, lastControlY = lastY + lastY - lastControlY,
52878 lastX = (+parts[i + 1]), lastY = (+parts[i + 2]));
52879 i += 2;
52880 }
52881 break;
52882 case 'h':
52883 while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
52884 me.lineTo(lastX += +parts[i], lastY);
52885 i++;
52886 }
52887 break;
52888 case 'H':
52889 while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
52890 me.lineTo(lastX = +parts[i], lastY);
52891 i++;
52892 }
52893 break;
52894 case 'v':
52895 while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
52896 me.lineTo(lastX, lastY += +parts[i]);
52897 i++;
52898 }
52899 break;
52900 case 'V':
52901 while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
52902 me.lineTo(lastX, lastY = +parts[i]);
52903 i++;
52904 }
52905 break;
52906 }
52907 }
52908 },
52909
52910 /**
52911 * @private
52912 * @param {Number} x1
52913 * @param {Number} y1
52914 * @param {Number} x2
52915 * @param {Number} y2
52916 * @param {Number} x
52917 * @param {Number} y
52918 * @return {Number}
52919 */
52920 rayTestLine: function (x1, y1, x2, y2, x, y) {
52921 var cx;
52922 if (y1 === y2) {
52923 if (y === y1) {
52924 if (Math.min(x1, x2) <= x && x <= Math.max(x1, x2)) {
52925 return -1;
52926 }
52927 } else {
52928 return 0;
52929 }
52930 }
52931 if (y1 < y && y < y2 || y2 < y && y < y1) {
52932 cx = (y - y1) * (x2 - x1) / (y2 - y1) + x1;
52933 if (cx === x) {
52934 return -1;
52935 } else if (cx < x) {
52936 return 0;
52937 } else {
52938 return 1;
52939 }
52940 } else {
52941 return 0;
52942 }
52943 },
52944
52945 /**
52946 * @private
52947 * @param {Number} x1
52948 * @param {Number} y1
52949 * @param {Number} x2
52950 * @param {Number} y2
52951 * @param {Number} x3
52952 * @param {Number} y3
52953 * @param {Number} x4
52954 * @param {Number} y4
52955 * @param {Number} x
52956 * @param {Number} y
52957 * @param {Number} idx
52958 * @return {*}
52959 */
52960 rayTestCubicBezier: function (x1, y1, x2, y2, x3, y3, x4, y4, x, y, idx) {
52961 if (Math.min(x1, x2, x3, x4) <= x && x <= Math.max(x1, x2, x3, x4)) {
52962 if (Math.min(y1, y2, y3, y4) <= y && y <= Math.max(y1, y2, y3, y4)) {
52963 var me = this,
52964 solver = me.solvers[idx] || (me.solvers[idx] = Ext.draw.Solver.createBezierSolver(x1, x2, x3, x4)),
52965 result = solver.solve(y);
52966 return (+(x <= result[0] && 0 <= result[0] && result[0] <= 1)) +
52967 (+(x <= result[1] && 0 <= result[1] && result[1] <= 1)) +
52968 (+(x <= result[2] && 0 <= result[2] && result[2] <= 1));
52969 }
52970 }
52971 return 0;
52972 },
52973
52974 /**
52975 * Test whether the given point is on or inside the path.
52976 * @param {Number} x
52977 * @param {Number} y
52978 * @return {Boolean}
52979 */
52980 isPointInPath: function (x, y) {
52981 var me = this,
52982 i, j, count = 0, test = 0,
52983 types = me.types,
52984 coords = me.coords,
52985 ln = types.length, firstX = null, firstY = null, lastX = 0, lastY = 0;
52986 for (i = 0, j = 0; i < ln; i++) {
52987 switch (types[i]) {
52988 case 'M':
52989 if (firstX !== null) {
52990 test = me.rayTestLine(firstX, firstY, lastX, lastY, x, y);
52991 if (test < 0) {
52992 count += 1;
52993 } else {
52994 count += test;
52995 }
52996 }
52997 firstX = lastX = coords[j];
52998 firstY = lastY = coords[j + 1];
52999 j += 2;
53000 break;
53001 case 'L':
53002 test = me.rayTestLine(lastX, lastY, coords[j], coords[j + 1], x, y);
53003 if (test < 0) {
53004 return true;
53005 }
53006 count += test;
53007 lastX = coords[j];
53008 lastY = coords[j + 1];
53009 j += 2;
53010 break;
53011 case 'C':
53012 test = me.rayTestCubicBezier(
53013 lastX, lastY,
53014 coords[j], coords[j + 1],
53015 coords[j + 2], coords[j + 3],
53016 coords[j + 4], coords[j + 5],
53017 x, y, i);
53018 if (test < 0) {
53019 return true;
53020 }
53021 count += test;
53022 lastX = coords[j + 4];
53023 lastY = coords[j + 5];
53024 j += 6;
53025 break;
53026 case 'Z':
53027 break;
53028 }
53029 }
53030 return count % 2 === 1;
53031 },
53032
53033 /**
53034 * Clone this path.
53035 * @return {Ext.draw.Path}
53036 */
53037 clone: function () {
53038 var me = this,
53039 path = new Ext.draw.Path();
53040 path.coords = me.coords.slice(0);
53041 path.types = me.types.slice(0);
53042 path.cursor = me.cursor ? me.cursor.slice(0) : null;
53043 path.startX = me.startX;
53044 path.startY = me.startY;
53045 path.svgString = me.svgString;
53046 return path;
53047 },
53048
53049 /**
53050 * Transform the current path by a matrix.
53051 * @param {Ext.draw.Matrix} matrix
53052 */
53053 transform: function (matrix) {
53054 if (matrix.isIdentity()) {
53055 return;
53056 }
53057 var xx = matrix.getXX(), yx = matrix.getYX(), dx = matrix.getDX(),
53058 xy = matrix.getXY(), yy = matrix.getYY(), dy = matrix.getDY(),
53059 coords = this.coords,
53060 i = 0, ln = coords.length,
53061 x, y;
53062
53063 for (; i < ln; i += 2) {
53064 x = coords[i];
53065 y = coords[i + 1];
53066 coords[i] = x * xx + y * yx + dx;
53067 coords[i + 1] = x * xy + y * yy + dy;
53068 }
53069 this.dirt();
53070 },
53071
53072 /**
53073 * Get the bounding box of this matrix.
53074 * @param {Object} [target] Optional object to receive the result.
53075 *
53076 * @return {Object} Object with x, y, width and height
53077 */
53078 getDimension: function (target) {
53079 if (!target) {
53080 target = {};
53081 }
53082
53083 if (!this.types || !this.types.length) {
53084 target.x = 0;
53085 target.y = 0;
53086 target.width = 0;
53087 target.height = 0;
53088 return target;
53089 }
53090
53091 target.left = Infinity;
53092 target.top = Infinity;
53093 target.right = -Infinity;
53094 target.bottom = -Infinity;
53095 var i = 0, j = 0,
53096 types = this.types,
53097 coords = this.coords,
53098 ln = types.length, x, y;
53099 for (; i < ln; i++) {
53100 switch (types[i]) {
53101 case 'M':
53102 case 'L':
53103 x = coords[j];
53104 y = coords[j + 1];
53105 target.left = Math.min(x, target.left);
53106 target.top = Math.min(y, target.top);
53107 target.right = Math.max(x, target.right);
53108 target.bottom = Math.max(y, target.bottom);
53109 j += 2;
53110 break;
53111 case 'C':
53112 this.expandDimension(target, x, y,
53113 coords[j], coords[j + 1],
53114 coords[j + 2], coords[j + 3],
53115 x = coords[j + 4], y = coords[j + 5]);
53116 j += 6;
53117 break;
53118 }
53119 }
53120
53121 target.x = target.left;
53122 target.y = target.top;
53123 target.width = target.right - target.left;
53124 target.height = target.bottom - target.top;
53125 return target;
53126 },
53127
53128 /**
53129 * Get the bounding box as if the path is transformed by a matrix.
53130 *
53131 * @param {Ext.draw.Matrix} matrix
53132 * @param {Object} [target] Optional object to receive the result.
53133 *
53134 * @return {Object} An object with x, y, width and height.
53135 */
53136 getDimensionWithTransform: function (matrix, target) {
53137 if (!this.types || !this.types.length) {
53138 if (!target) {
53139 target = {};
53140 }
53141 target.x = 0;
53142 target.y = 0;
53143 target.width = 0;
53144 target.height = 0;
53145 return target;
53146 }
53147
53148 target.left = Infinity;
53149 target.top = Infinity;
53150 target.right = -Infinity;
53151 target.bottom = -Infinity;
53152
53153 var xx = matrix.getXX(), yx = matrix.getYX(), dx = matrix.getDX(),
53154 xy = matrix.getXY(), yy = matrix.getYY(), dy = matrix.getDY(),
53155 i = 0, j = 0,
53156 types = this.types,
53157 coords = this.coords,
53158 ln = types.length, x, y;
53159 for (; i < ln; i++) {
53160 switch (types[i]) {
53161 case 'M':
53162 case 'L':
53163 x = coords[j] * xx + coords[j + 1] * yx + dx;
53164 y = coords[j] * xy + coords[j + 1] * yy + dy;
53165 target.left = Math.min(x, target.left);
53166 target.top = Math.min(y, target.top);
53167 target.right = Math.max(x, target.right);
53168 target.bottom = Math.max(y, target.bottom);
53169 j += 2;
53170 break;
53171 case 'C':
53172 this.expandDimension(target,
53173 x, y,
53174 coords[j] * xx + coords[j + 1] * yx + dx,
53175 coords[j] * xy + coords[j + 1] * yy + dy,
53176 coords[j + 2] * xx + coords[j + 3] * yx + dx,
53177 coords[j + 2] * xy + coords[j + 3] * yy + dy,
53178 x = coords[j + 4] * xx + coords[j + 5] * yx + dx,
53179 y = coords[j + 4] * xy + coords[j + 5] * yy + dy);
53180 j += 6;
53181 break;
53182 }
53183 }
53184 if (!target) {
53185 target = {};
53186 }
53187 target.x = target.left;
53188 target.y = target.top;
53189 target.width = target.right - target.left;
53190 target.height = target.bottom - target.top;
53191 return target;
53192 },
53193
53194 /**
53195 * @private
53196 * Expand the rect by the bbox of a bezier curve.
53197 *
53198 * @param {Object} target
53199 * @param {Number} x1
53200 * @param {Number} y1
53201 * @param {Number} cx1
53202 * @param {Number} cy1
53203 * @param {Number} cx2
53204 * @param {Number} cy2
53205 * @param {Number} x2
53206 * @param {Number} y2
53207 */
53208 expandDimension: function (target, x1, y1, cx1, cy1, cx2, cy2, x2, y2) {
53209 var me = this,
53210 l = target.left, r = target.right, t = target.top, b = target.bottom,
53211 dim = me.dim || (me.dim = []);
53212
53213 me.curveDimension(x1, cx1, cx2, x2, dim);
53214 l = Math.min(l, dim[0]);
53215 r = Math.max(r, dim[1]);
53216
53217 me.curveDimension(y1, cy1, cy2, y2, dim);
53218 t = Math.min(t, dim[0]);
53219 b = Math.max(b, dim[1]);
53220
53221 target.left = l;
53222 target.right = r;
53223 target.top = t;
53224 target.bottom = b;
53225 },
53226
53227 /**
53228 * @private
53229 * Determine the curve
53230 * @param {Number} a
53231 * @param {Number} b
53232 * @param {Number} c
53233 * @param {Number} d
53234 * @param {Number} dim
53235 */
53236 curveDimension: function (a, b, c, d, dim) {
53237 var qa = 3 * (-a + 3 * (b - c) + d),
53238 qb = 6 * (a - 2 * b + c),
53239 qc = -3 * (a - b), x, y,
53240 min = Math.min(a, d),
53241 max = Math.max(a, d), delta;
53242
53243 if (qa === 0) {
53244 if (qb === 0) {
53245 dim[0] = min;
53246 dim[1] = max;
53247 return;
53248 } else {
53249 x = -qc / qb;
53250 if (0 < x && x < 1) {
53251 y = this.interpolate(a, b, c, d, x);
53252 min = Math.min(min, y);
53253 max = Math.max(max, y);
53254 }
53255 }
53256 } else {
53257 delta = qb * qb - 4 * qa * qc;
53258 if (delta >= 0) {
53259 delta = Math.sqrt(delta);
53260 x = (delta - qb) / 2 / qa;
53261 if (0 < x && x < 1) {
53262 y = this.interpolate(a, b, c, d, x);
53263 min = Math.min(min, y);
53264 max = Math.max(max, y);
53265 }
53266 if (delta > 0) {
53267 x -= delta / qa;
53268 if (0 < x && x < 1) {
53269 y = this.interpolate(a, b, c, d, x);
53270 min = Math.min(min, y);
53271 max = Math.max(max, y);
53272 }
53273 }
53274 }
53275 }
53276 dim[0] = min;
53277 dim[1] = max;
53278 },
53279
53280 /**
53281 * @private
53282 *
53283 * Returns `a * (1 - t) ^ 3 + 3 * b (1 - t) ^ 2 * t + 3 * c (1 - t) * t ^ 3 + d * t ^ 3`.
53284 *
53285 * @param {Number} a
53286 * @param {Number} b
53287 * @param {Number} c
53288 * @param {Number} d
53289 * @param {Number} t
53290 * @return {Number}
53291 */
53292 interpolate: function (a, b, c, d, t) {
53293 if (t === 0) {
53294 return a;
53295 }
53296 if (t === 1) {
53297 return d;
53298 }
53299 var rate = (1 - t) / t;
53300 return t * t * t * (d + rate * (3 * c + rate * (3 * b + rate * a)));
53301 },
53302
53303 /**
53304 * Reconstruct path from cubic bezier curve stripes.
53305 * @param {Array} stripes
53306 */
53307 fromStripes: function (stripes) {
53308 var me = this,
53309 i = 0, ln = stripes.length,
53310 j, ln2, stripe;
53311 me.clear();
53312 for (; i < ln; i++) {
53313 stripe = stripes[i];
53314 me.coords.push.apply(me.coords, stripe);
53315 me.types.push('M');
53316 for (j = 2, ln2 = stripe.length; j < ln2; j += 6) {
53317 me.types.push('C');
53318 }
53319 }
53320 if (!me.cursor) {
53321 me.cursor = [];
53322 }
53323 me.cursor[0] = me.coords[me.coords.length - 2];
53324 me.cursor[1] = me.coords[me.coords.length - 1];
53325 me.dirt();
53326 },
53327
53328 /**
53329 * Convert path to bezier curve stripes.
53330 * @param {Array} [target] The optional array to receive the result.
53331 * @return {Array}
53332 */
53333 toStripes: function (target) {
53334 var stripes = target || [], curr,
53335 x, y, lastX, lastY, startX, startY,
53336 i, j,
53337 types = this.types,
53338 coords = this.coords,
53339 ln = types.length;
53340 for (i = 0, j = 0; i < ln; i++) {
53341 switch (types[i]) {
53342 case 'M':
53343 curr = [startX = lastX = coords[j++], startY = lastY = coords[j++]];
53344 stripes.push(curr);
53345 break;
53346 case 'L':
53347 x = coords[j++];
53348 y = coords[j++];
53349 curr.push((lastX + lastX + x) / 3, (lastY + lastY + y) / 3, (lastX + x + x) / 3, (lastY + y + y) / 3, lastX = x, lastY = y);
53350 break;
53351 case 'C':
53352 curr.push(coords[j++], coords[j++], coords[j++], coords[j++], lastX = coords[j++], lastY = coords[j++]);
53353 break;
53354 case 'Z':
53355 x = startX;
53356 y = startY;
53357 curr.push((lastX + lastX + x) / 3, (lastY + lastY + y) / 3, (lastX + x + x) / 3, (lastY + y + y) / 3, lastX = x, lastY = y);
53358 break;
53359 }
53360 }
53361 return stripes;
53362 },
53363
53364 /**
53365 * @private
53366 * Update cache for svg string of this path.
53367 */
53368 updateSvgString: function () {
53369 var result = [],
53370 types = this.types,
53371 coords = this.coords,
53372 ln = types.length,
53373 i = 0, j = 0;
53374 for (; i < ln; i++) {
53375 switch (types[i]) {
53376 case 'M':
53377 result.push('M' + coords[j] + ',' + coords[j + 1]);
53378 j += 2;
53379 break;
53380 case 'L':
53381 result.push('L' + coords[j] + ',' + coords[j + 1]);
53382 j += 2;
53383 break;
53384 case 'C':
53385 result.push('C' + coords[j] + ',' + coords[j + 1] + ' ' +
53386 coords[j + 2] + ',' + coords[j + 3] + ' ' +
53387 coords[j + 4] + ',' + coords[j + 5]);
53388 j += 6;
53389 break;
53390 case 'Z':
53391 result.push('Z');
53392 break;
53393 }
53394 }
53395 this.svgString = result.join('');
53396 },
53397
53398 /**
53399 * Return an svg path string for this path.
53400 * @return {String}
53401 */
53402 toString: function () {
53403 if (!this.svgString) {
53404 this.updateSvgString();
53405 }
53406 return this.svgString;
53407 }
53408 });
53409
53410 /**
53411 * @class Ext.draw.sprite.Path
53412 * @extends Ext.draw.sprite.Sprite
53413 *
53414 * A sprite that represents a path.
53415 *
53416 * @example preview miniphone
53417 * var component = new Ext.draw.Component({
53418 * items: [{
53419 * type: 'path',
53420 * path: 'M75,75 c0,-25 50,25 50,0 c0,-25 -50,25 -50,0',
53421 * fillStyle: 'blue'
53422 * }]
53423 * });
53424 * Ext.Viewport.setLayout('fit');
53425 * Ext.Viewport.add(component);
53426 */
53427 Ext.define('Ext.draw.sprite.Path', {
53428 extend: Ext.draw.sprite.Sprite ,
53429
53430 alias: 'sprite.path',
53431 type: 'path',
53432 inheritableStatics: {
53433 def: {
53434 processors: {
53435 /**
53436 * @cfg {String} path The SVG based path string used by the sprite.
53437 */
53438 path: function (n, o) {
53439 if (!(n instanceof Ext.draw.Path)) {
53440 n = new Ext.draw.Path(n);
53441 }
53442 return n;
53443 }
53444 },
53445 aliases: {
53446 d: 'path'
53447 },
53448 dirtyTriggers: {
53449 path: 'bbox'
53450 },
53451 updaters: {
53452 path: function (attr) {
53453 var path = attr.path;
53454 if (!path || path.bindAttr !== attr) {
53455 path = new Ext.draw.Path();
53456 path.bindAttr = attr;
53457 attr.path = path;
53458 }
53459 path.clear();
53460 this.updatePath(path, attr);
53461 attr.dirtyFlags.bbox = ['path'];
53462 }
53463 }
53464 }
53465 },
53466
53467 updatePlainBBox: function (plain) {
53468 if (this.attr.path) {
53469 this.attr.path.getDimension(plain);
53470 }
53471 },
53472
53473 updateTransformedBBox: function (transform) {
53474 if (this.attr.path) {
53475 this.attr.path.getDimensionWithTransform(this.attr.matrix, transform);
53476 }
53477 },
53478
53479 render: function (surface, ctx) {
53480 var mat = this.attr.matrix,
53481 attr = this.attr;
53482 if (!attr.path || attr.path.coords.length === 0) {
53483 return;
53484 }
53485 mat.toContext(ctx);
53486 ctx.appendPath(attr.path);
53487 ctx.fillStroke(attr);
53488 },
53489
53490 /**
53491 * Update the path.
53492 * @param {Ext.draw.Path} path An empty path to draw on using path API.
53493 * @param {Object} attr The attribute object. Note: DO NOT use the `sprite.attr` instead of this
53494 * if you want to work with instancing.
53495 */
53496 updatePath: function (path, attr) {}
53497 });
53498
53499 /**
53500 * @class Ext.draw.sprite.Circle
53501 * @extends Ext.draw.sprite.Path
53502 *
53503 * A sprite that represents a circle.
53504 *
53505 * @example preview miniphone
53506 * new Ext.draw.Component({
53507 * fullscreen: true,
53508 * items: [{
53509 * type: 'circle',
53510 * cx: 100,
53511 * cy: 100,
53512 * r: 25,
53513 * fillStyle: 'blue'
53514 * }]
53515 * });
53516 *
53517 */
53518 Ext.define("Ext.draw.sprite.Circle", {
53519 extend: Ext.draw.sprite.Path ,
53520 alias: 'sprite.circle',
53521 type: 'circle',
53522 inheritableStatics: {
53523 def: {
53524 processors: {
53525 /**
53526 * @cfg {Number} [cx=0] The center coordinate of the sprite on the x-axis.
53527 */
53528 cx: "number",
53529
53530 /**
53531 * @cfg {Number} [cy=0] The center coordinate of the sprite on the y-axis.
53532 */
53533 cy: "number",
53534
53535 /**
53536 * @cfg {Number} [r=0] The radius of the sprite.
53537 */
53538 r: "number"
53539 },
53540 aliases: {
53541 radius: "r",
53542 x: "cx",
53543 y: "cy",
53544 centerX: "cx",
53545 centerY: "cy"
53546 },
53547 defaults: {
53548 cx: 0,
53549 cy: 0,
53550 r: 0
53551 },
53552 dirtyTriggers: {
53553 cx: 'path',
53554 cy: 'path',
53555 r: 'path'
53556 }
53557 }
53558 },
53559
53560 updatePlainBBox: function (plain) {
53561 var attr = this.attr,
53562 cx = attr.cx,
53563 cy = attr.cy,
53564 r = attr.r;
53565 plain.x = cx - r;
53566 plain.y = cy - r;
53567 plain.width = r + r;
53568 plain.height = r + r;
53569 },
53570
53571 updateTransformedBBox: function (transform) {
53572 var attr = this.attr,
53573 cx = attr.cx,
53574 cy = attr.cy,
53575 r = attr.r,
53576 matrix = attr.matrix,
53577 scalesX = matrix.getScaleX(),
53578 scalesY = matrix.getScaleY(),
53579 w, h;
53580 w = scalesX * r;
53581 h = scalesY * r;
53582 transform.x = matrix.x(cx, cy) - w;
53583 transform.y = matrix.y(cx, cy) - h;
53584 transform.width = w + w;
53585 transform.height = h + h;
53586 },
53587
53588 updatePath: function (path, attr) {
53589 path.arc(attr.cx, attr.cy, attr.r, 0, Math.PI * 2, false);
53590 }
53591 });
53592
53593 /**
53594 * @class Ext.draw.sprite.Ellipse
53595 * @extends Ext.draw.sprite.Path
53596 *
53597 * A sprite that represents an ellipse.
53598 *
53599 * @example preview miniphone
53600 * var component = new Ext.draw.Component({
53601 * items: [{
53602 * type: 'ellipse',
53603 * cx: 100,
53604 * cy: 100,
53605 * rx: 40,
53606 * ry: 25,
53607 * fillStyle: 'blue'
53608 * }]
53609 * });
53610 * Ext.Viewport.setLayout('fit');
53611 * Ext.Viewport.add(component);
53612 */
53613 Ext.define("Ext.draw.sprite.Ellipse", {
53614 extend: Ext.draw.sprite.Path ,
53615 alias: 'sprite.ellipse',
53616 type: 'circle',
53617 inheritableStatics: {
53618 def: {
53619 processors: {
53620 /**
53621 * @cfg {Number} [cx=0] The center coordinate of the sprite on the x-axis.
53622 */
53623 cx: "number",
53624
53625 /**
53626 * @cfg {Number} [cy=0] The center coordinate of the sprite on the y-axis.
53627 */
53628 cy: "number",
53629
53630 /**
53631 * @cfg {Number} [rx=1] The radius of the sprite on the x-axis.
53632 */
53633 rx: "number",
53634
53635 /**
53636 * @cfg {Number} [ry=1] The radius of the sprite on the y-axis.
53637 */
53638 ry: "number",
53639
53640 /**
53641 * @cfg {Number} [axisRotation=0] The rotation of the sprite about its axis.
53642 */
53643 axisRotation: "number"
53644 },
53645 aliases: {
53646 radius: "r",
53647 x: "cx",
53648 y: "cy",
53649 centerX: "cx",
53650 centerY: "cy",
53651 radiusX: "rx",
53652 radiusY: "ry"
53653 },
53654 defaults: {
53655 cx: 0,
53656 cy: 0,
53657 rx: 1,
53658 ry: 1,
53659 axisRotation: 0
53660 },
53661 dirtyTriggers: {
53662 cx: 'path',
53663 cy: 'path',
53664 rx: 'path',
53665 ry: 'path',
53666 axisRotation: 'path'
53667 }
53668 }
53669 },
53670
53671 updatePlainBBox: function (plain) {
53672 var attr = this.attr,
53673 cx = attr.cx,
53674 cy = attr.cy,
53675 rx = attr.rx,
53676 ry = attr.ry;
53677 plain.x = cx - rx;
53678 plain.y = cy - ry;
53679 plain.width = rx + rx;
53680 plain.height = ry + ry;
53681 },
53682
53683 updateTransformedBBox: function (transform) {
53684 var attr = this.attr,
53685 cx = attr.cx,
53686 cy = attr.cy,
53687 rx = attr.rx,
53688 ry = attr.ry,
53689 rxy = ry / rx,
53690 matrix = attr.matrix.clone(),
53691 xx, xy, yx, yy, dx, dy, w, h;
53692 matrix.append(1, 0, 0, rxy, 0, cy * (1 - rxy));
53693 xx = matrix.getXX();
53694 yx = matrix.getYX();
53695 dx = matrix.getDX();
53696 xy = matrix.getXY();
53697 yy = matrix.getYY();
53698 dy = matrix.getDY();
53699 w = Math.sqrt(xx * xx + yx * yx) * rx;
53700 h = Math.sqrt(xy * xy + yy * yy) * rx;
53701 transform.x = cx * xx + cy * yx + dx - w;
53702 transform.y = cx * xy + cy * yy + dy - h;
53703 transform.width = w + w;
53704 transform.height = h + h;
53705 },
53706
53707 updatePath: function (path, attr) {
53708 path.ellipse(attr.cx, attr.cy, attr.rx, attr.ry, attr.axisRotation, 0, Math.PI * 2, false);
53709 }
53710 });
53711
53712 /**
53713 * Utility class to provide a way to *approximately* measure the dimension of texts without a drawing context.
53714 */
53715 Ext.define("Ext.draw.TextMeasurer", {
53716 singleton: true,
53717
53718
53719
53720 measureDiv: null,
53721 measureCache: {},
53722
53723 /**
53724 * @private Measure the size of a text with specific font by using DOM to measure it.
53725 * Could be very expensive therefore should be used lazily.
53726 * @param {String} text
53727 * @param {String} font
53728 * @return {Object} An object with `width` and `height` properties.
53729 * @return {Number} return.width
53730 * @return {Number} return.height
53731 */
53732 actualMeasureText: function (text, font) {
53733 var me = Ext.draw.TextMeasurer,
53734 measureDiv = me.measureDiv,
53735 FARAWAY = 100000,
53736 size;
53737
53738 if (!measureDiv) {
53739 var parent = Ext.Element.create({
53740 style: {
53741 "overflow": "hidden",
53742 "position": "relative",
53743 "float": "left", // DO NOT REMOVE THE QUOTE OR IT WILL BREAK COMPRESSOR
53744 "width": 0,
53745 "height": 0
53746 }
53747 });
53748 me.measureDiv = measureDiv = Ext.Element.create({});
53749 measureDiv.setStyle({
53750 "position": 'absolute',
53751 "x": FARAWAY,
53752 "y": FARAWAY,
53753 "z-index": -FARAWAY,
53754 "white-space": "nowrap",
53755 "display": 'block',
53756 "padding": 0,
53757 "margin": 0
53758 });
53759 Ext.getBody().appendChild(parent);
53760 parent.appendChild(measureDiv);
53761 }
53762 if (font) {
53763 measureDiv.setStyle({
53764 font: font,
53765 lineHeight: 'normal'
53766 });
53767 }
53768 measureDiv.setText('(' + text + ')');
53769 size = measureDiv.getSize();
53770 measureDiv.setText('()');
53771 size.width -= measureDiv.getSize().width;
53772 return size;
53773 },
53774
53775 /**
53776 * Measure a single-line text with specific font.
53777 * This will split the text to characters and add up their size.
53778 * That may *not* be the exact size of the text as it is displayed.
53779 * @param {String} text
53780 * @param {String} font
53781 * @return {Object} An object with `width` and `height` properties.
53782 * @return {Number} return.width
53783 * @return {Number} return.height
53784 */
53785 measureTextSingleLine: function (text, font) {
53786 text = text.toString();
53787 var cache = this.measureCache,
53788 chars = text.split(''),
53789 width = 0,
53790 height = 0,
53791 cachedItem, charactor, i, ln, size;
53792
53793 if (!cache[font]) {
53794 cache[font] = {};
53795 }
53796 cache = cache[font];
53797
53798 if (cache[text]) {
53799 return cache[text];
53800 }
53801
53802 for (i = 0, ln = chars.length; i < ln; i++) {
53803 charactor = chars[i];
53804 if (!(cachedItem = cache[charactor])) {
53805 size = this.actualMeasureText(charactor, font);
53806 cachedItem = cache[charactor] = size;
53807 }
53808 width += cachedItem.width;
53809 height = Math.max(height, cachedItem.height);
53810 }
53811 return cache[text] = {
53812 width: width,
53813 height: height
53814 };
53815 },
53816
53817 /**
53818 * Measure a text with specific font.
53819 * This will split the text to lines and add up their size.
53820 * That may *not* be the exact size of the text as it is displayed.
53821 * @param {String} text
53822 * @param {String} font
53823 * @return {Object} An object with `width`, `height` and `sizes` properties.
53824 * @return {Number} return.width
53825 * @return {Number} return.height
53826 * @return {Array} return.sizes Results of individual line measurements, in case of multiline text.
53827 */
53828 measureText: function (text, font) {
53829 var lines = text.split('\n'),
53830 ln = lines.length,
53831 height = 0,
53832 width = 0,
53833 line, i,
53834 sizes;
53835
53836 if (ln === 1) {
53837 return this.measureTextSingleLine(text, font);
53838 }
53839
53840 sizes = [];
53841 for (i = 0; i < ln; i++) {
53842 line = this.measureTextSingleLine(lines[i], font);
53843 sizes.push(line);
53844 height += line.height;
53845 width = Math.max(width, line.width);
53846 }
53847
53848 return {
53849 width: width,
53850 height: height,
53851 sizes: sizes
53852 };
53853 }
53854 });
53855
53856 /**
53857 * @class Ext.draw.sprite.Text
53858 * @extends Ext.draw.sprite.Sprite
53859 *
53860 * A sprite that represents text.
53861 *
53862 * @example preview miniphone
53863 * var component = new Ext.draw.Component({
53864 * items: [{
53865 * type: 'text',
53866 * x: 50,
53867 * y: 50,
53868 * text: 'Sencha',
53869 * fontSize: 18,
53870 * fillStyle: 'blue'
53871 * }]
53872 * });
53873 * Ext.Viewport.setLayout('fit');
53874 * Ext.Viewport.add(component);
53875 */
53876 Ext.define("Ext.draw.sprite.Text", {
53877 extend: Ext.draw.sprite.Sprite ,
53878
53879 alias: 'sprite.text',
53880 type: 'text',
53881 lineBreakRe: /\n/g,
53882 inheritableStatics: {
53883 shortHand1Re: /'(.*)'/g,
53884 shortHand2Re: / /g,
53885 shortHand3Re: /\s*,\s*/g,
53886 shortHand4Re: /\$\$\$\$/g,
53887 def: {
53888 processors: {
53889 /**
53890 * @cfg {Number} [x=0] The position of the sprite on the x-axis.
53891 */
53892 x: "number",
53893
53894 /**
53895 * @cfg {Number} [y=0] The position of the sprite on the y-axis.
53896 */
53897 y: "number",
53898
53899 /**
53900 * @cfg {String} [text=''] The text represented in the sprite.
53901 */
53902 text: "string",
53903
53904 /**
53905 * @cfg {String/Number} [fontSize='10px'] The size of the font displayed.
53906 */
53907 fontSize: function (n) {
53908 if (!isNaN(n)) {
53909 return +n + 'px';
53910 } else if (n.match(Ext.dom.Element.unitRe)) {
53911 return n;
53912 }
53913 },
53914
53915 /**
53916 * @cfg {String} [fontStyle=''] The style of the font displayed. {normal, italic, oblique}
53917 */
53918 fontStyle: "enums(,italic,oblique)",
53919
53920 /**
53921 * @cfg {String} [fontVariant=''] The variant of the font displayed. {normal, small-caps}
53922 */
53923 fontVariant: "enums(,small-caps)",
53924
53925 /**
53926 * @cfg {String} [fontWeight=''] The weight of the font displayed. {normal, bold, bolder, lighter}
53927 */
53928 fontWeight: (function (fontWeights) {
53929 return function (n) {
53930 if (!n) {
53931 return "";
53932 } else if (n === 'normal') {
53933 return '';
53934 } else if (!isNaN(n)) {
53935 n = +n;
53936 if (100 <= n && n <= 900) {
53937 return n;
53938 }
53939 } else if (n in fontWeights) {
53940 return n;
53941 }
53942 };
53943 })({"normal": true, "bold": true, "bolder": true, "lighter": true}),
53944
53945 /**
53946 * @cfg {String} [fontFamily='sans-serif'] The family of the font displayed.
53947 */
53948 fontFamily: "string",
53949
53950 /**
53951 * @cfg {String} [textAlign='start'] The alignment of the text displayed. {left, right, center, start, end}
53952 */
53953 textAlign: (function (textAligns) {
53954 return function (n) {
53955 if (n === 'middle') {
53956 return 'center';
53957 } else if (!n) {
53958 return "center";
53959 } else if (!Ext.isString(n)) {
53960 return undefined;
53961 } else if (n in textAligns) {
53962 return n;
53963 }
53964 };
53965 })({"left": true, "right": true, "center": true, "start": true, "end": true}),
53966
53967 /**
53968 * @cfg {String} [textBaseline="alphabetic"] The baseline of the text displayed. {top, hanging, middle, alphabetic, ideographic, bottom}
53969 */
53970 textBaseline: (function (textBaselines) {
53971 return function (n) {
53972 if (n === false) {
53973 return "alphabetic";
53974 } else if (n in textBaselines) {
53975 return n;
53976 } else if (n === 'center') {
53977 return 'middle';
53978 }
53979 };
53980 })({"top": true, "hanging": true, "middle": true, "alphabetic": true, "ideographic": true, "bottom": true}),
53981
53982 /**
53983 * @cfg {String} [font='10px sans-serif'] The font displayed.
53984 */
53985 font: "string"
53986 },
53987 aliases: {
53988 "font-size": "fontSize",
53989 "font-family": "fontFamily",
53990 "font-weight": "fontWeight",
53991 "font-variant": "fontVariant",
53992 "text-anchor": "textAlign"
53993 },
53994 defaults: {
53995 fontStyle: '',
53996 fontVariant: '',
53997 fontWeight: '',
53998 fontSize: '10px',
53999 fontFamily: 'sans-serif',
54000 font: '10px sans-serif',
54001 textBaseline: "alphabetic",
54002 textAlign: "start",
54003 strokeStyle: 'rgba(0, 0, 0, 0)',
54004 divBased: true,
54005 fillStyle: '#000',
54006 x: 0,
54007 y: 0,
54008 text: ''
54009 },
54010 dirtyTriggers: {
54011 fontStyle: 'font,bbox',
54012 fontVariant: 'font,bbox',
54013 fontWeight: 'font,bbox',
54014 fontSize: 'font,bbox',
54015 fontFamily: 'font,bbox',
54016 font: 'font-short-hand,bbox,canvas',
54017 textBaseline: 'bbox',
54018 textAlign: 'bbox',
54019 x: "bbox",
54020 y: "bbox",
54021 text: "bbox"
54022 },
54023 updaters: {
54024 "font-short-hand": (function (dispatcher) {
54025 return function (attrs) {
54026 // TODO: Do this according to http://www.w3.org/TR/CSS21/fonts.html#font-shorthand
54027 var value = attrs.font,
54028 parts, part, i, ln, dispKey;
54029 value = value.replace(Ext.draw.sprite.Text.shortHand1Re, function (a, arg1) {
54030 return arg1.replace(Ext.draw.sprite.Text.shortHand2Re, '$$$$');
54031 });
54032 value = value.replace(Ext.draw.sprite.Text.shortHand3Re, ',');
54033 parts = value.split(' ');
54034
54035 attrs = {};
54036 for (i = 0, ln = parts.length; i < ln; i++) {
54037 part = parts[i];
54038 dispKey = dispatcher[part];
54039 if (dispKey) {
54040 attrs[dispKey] = part;
54041 } else if (part.match(Ext.dom.Element.unitRe)) {
54042 attrs.fontSize = part;
54043 } else {
54044 attrs.fontFamily = part.replace(Ext.draw.sprite.Text.shortHand4Re, ' ');
54045 }
54046 }
54047 this.setAttributes(attrs, true);
54048 };
54049 })({
54050 "italic": "fontStyles",
54051 "oblique": "fontStyles",
54052 "bold": "fontWeights",
54053 "bolder": "fontWeights",
54054 "lighter": "fontWeights",
54055 "100": "fontWeights",
54056 "200": "fontWeights",
54057 "300": "fontWeights",
54058 "400": "fontWeights",
54059 "500": "fontWeights",
54060 "600": "fontWeights",
54061 "700": "fontWeights",
54062 "800": "fontWeights",
54063 "900": "fontWeights",
54064 "small-caps": "fontVariant"
54065 }),
54066 "font": function (attrs) {
54067 var font = '';
54068 if (attrs.fontWeight) {
54069 font += attrs.fontWeight + ' ';
54070 }
54071 if (attrs.fontVariant) {
54072 font += attrs.fontVariant + ' ';
54073 }
54074 if (attrs.fontSize) {
54075 font += attrs.fontSize + ' ';
54076 }
54077 if (attrs.fontFamily) {
54078 font += attrs.fontFamily + ' ';
54079 }
54080 this.setAttributes({
54081 font: font.substr(0, font.length - 1)
54082 }, true);
54083 }
54084 }
54085 }
54086 },
54087
54088 constructor: function (config) {
54089 Ext.draw.sprite.Sprite.prototype.constructor.call(this, config);
54090 },
54091
54092 updatePlainBBox: function (plain) {
54093 var me = this,
54094 attr = me.attr,
54095 x = attr.x,
54096 y = attr.y,
54097 dx = [],
54098 font = attr.font,
54099 text = attr.text,
54100 baseline = attr.textBaseline,
54101 alignment = attr.textAlign,
54102 size = Ext.draw.TextMeasurer.measureText(text, font),
54103 sizes = size.sizes,
54104 height = size.height,
54105 width = size.width,
54106 ln = sizes ? sizes.length : 0,
54107 i = 0;
54108
54109 switch (baseline) {
54110 case 'hanging' :
54111 case 'top':
54112 break;
54113 case 'ideographic' :
54114 case 'bottom' :
54115 y -= height;
54116 break;
54117 case 'alphabetic' :
54118 y -= height * 0.8;
54119 break;
54120 case 'middle' :
54121 case 'center' :
54122 y -= height * 0.5;
54123 break;
54124 }
54125 switch (alignment) {
54126 case 'end' :
54127 case 'right' :
54128 x -= width;
54129 for (; i < ln; i++) {
54130 dx.push(width - sizes[i].width);
54131 }
54132 break;
54133 case 'middle' :
54134 case 'center' :
54135 x -= width * 0.5;
54136 for (; i < ln; i++) {
54137 dx.push((width - sizes[i].width) * 0.5);
54138 }
54139 break;
54140 }
54141
54142 attr.textAlignOffsets = dx;
54143
54144 plain.x = x;
54145 plain.y = y;
54146 plain.width = width;
54147 plain.height = height;
54148 },
54149
54150 setText: function (text) {
54151 this.setAttributes({text: text}, true);
54152 },
54153
54154 setElementStyles: function (element, styles) {
54155 var stylesCache = element.stylesCache || (element.stylesCache = {}),
54156 style = element.dom.style,
54157 name;
54158 for (name in styles) {
54159 if (stylesCache[name] !== styles[name]) {
54160 stylesCache[name] = style[name] = styles[name];
54161 }
54162 }
54163 },
54164
54165 render: function (surface, ctx) {
54166 var attr = this.attr,
54167 mat = Ext.draw.Matrix.fly(attr.matrix.elements.slice(0)),
54168 bbox = this.getBBox(true),
54169 dx = attr.textAlignOffsets,
54170 x, y, i, lines;
54171 if (attr.text.length === 0) {
54172 return;
54173 }
54174
54175 lines = attr.text.split('\n');
54176 // Simulate textBaseline and textAlign.
54177 x = attr.bbox.plain.x;
54178 y = attr.bbox.plain.y;
54179 mat.toContext(ctx);
54180 for (i = 0; i < lines.length; i++) {
54181 if (ctx.fillStyle !== 'rgba(0, 0, 0, 0)') {
54182 ctx.fillText(lines[i], x + (dx[i] || 0), y + bbox.height / lines.length * i);
54183 }
54184 if (ctx.strokeStyle !== 'rgba(0, 0, 0, 0)') {
54185 ctx.strokeText(lines[i], x + (dx[i] || 0), y + bbox.height / lines.length * i);
54186 }
54187 }
54188 }
54189 });
54190
54191 /**
54192 * @class Ext.draw.sprite.EllipticalArc
54193 * @extends Ext.draw.sprite.Ellipse
54194 *
54195 * A sprite that represents an elliptical arc.
54196 *
54197 * @example preview miniphone
54198 * var component = new Ext.draw.Component({
54199 * items: [{
54200 * type: 'ellipticalArc',
54201 * cx: 100,
54202 * cy: 100,
54203 * rx: 40,
54204 * ry: 25,
54205 * fillStyle: 'blue',
54206 * startAngle: 0,
54207 * endAngle: Math.PI,
54208 * anticlockwise: true
54209 * }]
54210 * });
54211 * Ext.Viewport.setLayout('fit');
54212 * Ext.Viewport.add(component);
54213 */
54214 Ext.define("Ext.draw.sprite.EllipticalArc", {
54215 extend: Ext.draw.sprite.Ellipse ,
54216 alias: 'sprite.ellipticalArc',
54217 type: 'ellipticalArc',
54218 inheritableStatics: {
54219 def: {
54220 processors: {
54221 /**
54222 * @cfg {Number} [startAngle=0] The beginning angle of the arc.
54223 */
54224 startAngle: "number",
54225
54226 /**
54227 * @cfg {Number} [endAngle=Math.PI*2] The ending angle of the arc.
54228 */
54229 endAngle: "number",
54230
54231 /**
54232 * @cfg {Boolean} [anticlockwise=false] Determines whether or not the arc is drawn clockwise.
54233 */
54234 anticlockwise: "bool"
54235 },
54236 aliases: {
54237 from: "startAngle",
54238 to: "endAngle",
54239 start: "startAngle",
54240 end: "endAngle"
54241 },
54242 defaults: {
54243 startAngle: 0,
54244 endAngle: Math.PI * 2,
54245 anticlockwise: false
54246 },
54247 dirtyTriggers: {
54248 startAngle: 'path',
54249 endAngle: 'path',
54250 anticlockwise: 'path'
54251 }
54252 }
54253 },
54254
54255 updatePath: function (path, attr) {
54256 path.ellipse(attr.cx, attr.cy, attr.rx, attr.ry, attr.axisRotation, attr.startAngle, attr.endAngle, attr.anticlockwise);
54257 }
54258 });
54259
54260 /**
54261 * @class Ext.draw.sprite.Sector
54262 * @extends Ext.draw.sprite.Path
54263 *
54264 * A sprite representing a pie slice.
54265 */
54266 Ext.define('Ext.draw.sprite.Sector', {
54267 extend: Ext.draw.sprite.Path ,
54268 alias: 'sprite.sector',
54269 type: 'sector',
54270 inheritableStatics: {
54271 def: {
54272 processors: {
54273 /**
54274 * @cfg {Number} [centerX=0] The center coordinate of the sprite on the x-axis.
54275 */
54276 centerX: 'number',
54277
54278 /**
54279 * @cfg {Number} [centerY=0] The center coordinate of the sprite on the y-axis.
54280 */
54281 centerY: 'number',
54282
54283 /**
54284 * @cfg {Number} [startAngle=0] The starting angle of the sprite.
54285 */
54286 startAngle: 'number',
54287
54288 /**
54289 * @cfg {Number} [endAngle=0] The ending angle of the sprite.
54290 */
54291 endAngle: 'number',
54292
54293 /**
54294 * @cfg {Number} [startRho=0] The starting point of the radius of the sprite.
54295 */
54296 startRho: 'number',
54297
54298 /**
54299 * @cfg {Number} [endRho=150] The ending point of the radius of the sprite.
54300 */
54301 endRho: 'number',
54302
54303 /**
54304 * @cfg {Number} [margin=0] The margin of the sprite from the center of pie.
54305 */
54306 margin: 'number'
54307 },
54308 aliases: {
54309 rho: 'endRho'
54310 },
54311 dirtyTriggers: {
54312 centerX: 'path,bbox',
54313 centerY: 'path,bbox',
54314 startAngle: 'path,bbox',
54315 endAngle: 'path,bbox',
54316 startRho: 'path,bbox',
54317 endRho: 'path,bbox',
54318 margin: 'path,bbox'
54319 },
54320 defaults: {
54321 centerX: 0,
54322 centerY: 0,
54323 startAngle: 0,
54324 endAngle: 0,
54325 startRho: 0,
54326 endRho: 150,
54327 margin: 0,
54328 path: 'M 0,0'
54329 }
54330 }
54331 },
54332
54333 updatePath: function (path, attr) {
54334 var startAngle = Math.min(attr.startAngle, attr.endAngle),
54335 endAngle = Math.max(attr.startAngle, attr.endAngle),
54336 midAngle = (startAngle + endAngle) * 0.5,
54337 margin = attr.margin,
54338 centerX = attr.centerX,
54339 centerY = attr.centerY,
54340 startRho = Math.min(attr.startRho, attr.endRho),
54341 endRho = Math.max(attr.startRho, attr.endRho);
54342
54343 if (margin) {
54344 centerX += margin * Math.cos(midAngle);
54345 centerY += margin * Math.sin(midAngle);
54346 }
54347 path.moveTo(centerX + startRho * Math.cos(startAngle), centerY + startRho * Math.sin(startAngle));
54348 path.lineTo(centerX + endRho * Math.cos(startAngle), centerY + endRho * Math.sin(startAngle));
54349 path.arc(centerX, centerY, endRho, startAngle, endAngle, false);
54350 path.lineTo(centerX + startRho * Math.cos(endAngle), centerY + startRho * Math.sin(endAngle));
54351 path.arc(centerX, centerY, startRho, endAngle, startAngle, true);
54352 }
54353 });
54354
54355 /**
54356 * @class Ext.draw.sprite.Composite
54357 * @extends Ext.draw.sprite.Sprite
54358 *
54359 * Represents a group of sprites.
54360 */
54361 Ext.define('Ext.draw.sprite.Composite', {
54362 extend: Ext.draw.sprite.Sprite ,
54363 alias: 'sprite.composite',
54364 type: 'composite',
54365
54366 constructor: function () {
54367 this.callSuper(arguments);
54368 this.sprites = [];
54369 this.sprites.map = {};
54370 },
54371
54372 /**
54373 * Adds a sprite to the composite.
54374 * @param {Ext.draw.sprite.Sprite|Object} sprite
54375 */
54376 add: function (sprite) {
54377 if (!(sprite instanceof Ext.draw.sprite.Sprite)) {
54378 sprite = Ext.create('sprite.' + sprite.type, sprite);
54379 sprite.setParent(this);
54380 }
54381 var oldTransformations = sprite.applyTransformations,
54382 me = this,
54383 attr = me.attr;
54384
54385 sprite.applyTransformations = function () {
54386 if (sprite.attr.dirtyTransform) {
54387 attr.dirtyTransform = true;
54388 attr.bbox.plain.dirty = true;
54389 attr.bbox.transform.dirty = true;
54390 }
54391 oldTransformations.call(sprite);
54392 };
54393 this.sprites.push(sprite);
54394 this.sprites.map[sprite.id] = sprite.getId();
54395 attr.bbox.plain.dirty = true;
54396 attr.bbox.transform.dirty = true;
54397 return sprite;
54398 },
54399
54400 /**
54401 * Adds a list of sprites to the composite.
54402 * @param {Ext.draw.sprite.Sprite[]|Object[]|Ext.draw.sprite.Sprite|Object} sprites
54403 */
54404 addAll: function (sprites) {
54405 if (sprites.isSprite || sprites.type) {
54406 this.add(sprites);
54407 } else if (Ext.isArray(sprites)) {
54408 var i = 0;
54409 while (i < sprites.length) {
54410 this.add(sprites[i++]);
54411 }
54412 }
54413 },
54414
54415 /**
54416 * Updates the bounding box of the composite, which contains the bounding box of all sprites in the composite.
54417 */
54418 updatePlainBBox: function (plain) {
54419 var me = this,
54420 left = Infinity,
54421 right = -Infinity,
54422 top = Infinity,
54423 bottom = -Infinity,
54424 sprite, bbox, i, ln;
54425
54426 for (i = 0, ln = me.sprites.length; i < ln; i++) {
54427 sprite = me.sprites[i];
54428 sprite.applyTransformations();
54429 bbox = sprite.getBBox();
54430 if (left > bbox.x) {
54431 left = bbox.x;
54432 }
54433 if (right < bbox.x + bbox.width) {
54434 right = bbox.x + bbox.width;
54435 }
54436 if (top > bbox.y) {
54437 top = bbox.y;
54438 }
54439 if (bottom < bbox.y + bbox.height) {
54440 bottom = bbox.y + bbox.height;
54441 }
54442 }
54443 plain.x = left;
54444 plain.y = top;
54445 plain.width = right - left;
54446 plain.height = bottom - top;
54447 },
54448
54449 /**
54450 * Renders all sprites contained in the composite to the surface.
54451 */
54452 render: function (surface, ctx, region) {
54453 var mat = this.attr.matrix,
54454 i, ln;
54455
54456 mat.toContext(ctx);
54457 for (i = 0, ln = this.sprites.length; i < ln; i++) {
54458 surface.renderSprite(this.sprites[i], region);
54459 }
54460 }
54461 });
54462
54463 /**
54464 * @class Ext.draw.sprite.Arc
54465 * @extend Ext.draw.sprite.Circle
54466 *
54467 * A sprite that represents a circular arc.
54468 *
54469 * @example preview miniphone
54470 * var component = new Ext.draw.Component({
54471 * items: [{
54472 * type: 'arc',
54473 * cx: 100,
54474 * cy: 100,
54475 * r: 25,
54476 * fillStyle: 'blue',
54477 * startAngle: 0,
54478 * endAngle: Math.PI,
54479 * anticlockwise: true
54480 * }]
54481 * });
54482 * Ext.Viewport.setLayout('fit');
54483 * Ext.Viewport.add(component);
54484 */
54485 Ext.define("Ext.draw.sprite.Arc", {
54486 extend: Ext.draw.sprite.Circle ,
54487 alias: 'sprite.arc',
54488 type: 'arc',
54489 inheritableStatics: {
54490 def: {
54491 processors: {
54492 /**
54493 * @cfg {Number} [startAngle=0] The beginning angle of the arc.
54494 */
54495 startAngle: "number",
54496
54497 /**
54498 * @cfg {Number} [endAngle=Math.PI*2] The ending angle of the arc.
54499 */
54500 endAngle: "number",
54501
54502 /**
54503 * @cfg {Boolean} [anticlockwise=false] Determines whether or not the arc is drawn clockwise.
54504 */
54505 anticlockwise: "bool"
54506 },
54507 aliases: {
54508 from: "startAngle",
54509 to: "endAngle",
54510 start: "startAngle",
54511 end: "endAngle"
54512 },
54513 defaults: {
54514 startAngle: 0,
54515 endAngle: Math.PI * 2,
54516 anticlockwise: false
54517 },
54518 dirtyTriggers: {
54519 startAngle: 'path',
54520 endAngle: 'path',
54521 anticlockwise: 'path'
54522 }
54523 }
54524 },
54525
54526 updatePath: function (path, attr) {
54527 path.arc(attr.cx, attr.cy, attr.r, attr.startAngle, attr.endAngle, attr.anticlockwise);
54528 }
54529 });
54530
54531 /**
54532 * @class Ext.draw.sprite.Rect
54533 * @extends Ext.draw.sprite.Path
54534 *
54535 * A sprite that represents a rectangle.
54536 *
54537 * @example preview miniphone
54538 * var component = new Ext.draw.Component({
54539 * items: [{
54540 * type: 'rect',
54541 * x: 50,
54542 * y: 50,
54543 * width: 50,
54544 * height: 50,
54545 * fillStyle: 'blue'
54546 * }]
54547 * });
54548 * Ext.Viewport.setLayout('fit');
54549 * Ext.Viewport.add(component);
54550 */
54551 Ext.define('Ext.draw.sprite.Rect', {
54552 extend: Ext.draw.sprite.Path ,
54553 alias: 'sprite.rect',
54554 type: 'rect',
54555 inheritableStatics: {
54556 def: {
54557 processors: {
54558 /**
54559 * @cfg {Number} [x=0] The position of the sprite on the x-axis.
54560 */
54561 x: 'number',
54562
54563 /**
54564 * @cfg {Number} [y=0] The position of the sprite on the y-axis.
54565 */
54566 y: 'number',
54567
54568 /**
54569 * @cfg {Number} [width=1] The width of the sprite.
54570 */
54571 width: 'number',
54572
54573 /**
54574 * @cfg {Number} [height=1] The height of the sprite.
54575 */
54576 height: 'number',
54577
54578 /**
54579 * @cfg {Number} [radius=0] The radius of the rounded corners.
54580 */
54581 radius: 'number'
54582 },
54583 aliases: {
54584
54585 },
54586 dirtyTriggers: {
54587 x: 'path',
54588 y: 'path',
54589 width: 'path',
54590 height: 'path',
54591 radius: 'path'
54592 },
54593 defaults: {
54594 x: 0,
54595 y: 0,
54596 width: 1,
54597 height: 1,
54598 radius: 0
54599 }
54600 }
54601 },
54602
54603 updatePlainBBox: function (plain) {
54604 var attr = this.attr;
54605 plain.x = attr.x;
54606 plain.y = attr.y;
54607 plain.width = attr.width;
54608 plain.height = attr.height;
54609 },
54610
54611 updateTransformedBBox: function (transform, plain) {
54612 this.attr.matrix.transformBBox(plain, this.attr.radius, transform);
54613 },
54614
54615 updatePath: function (path, attr) {
54616 var x = attr.x,
54617 y = attr.y,
54618 width = attr.width,
54619 height = attr.height,
54620 radius = Math.min(attr.radius, Math.abs(attr.height) * 0.5, Math.abs(attr.width) * 0.5);
54621 if (radius === 0) {
54622 path.rect(x, y, width, height);
54623 } else {
54624 path.moveTo(x + radius, y);
54625 path.arcTo(x + width, y, x + width, y + height, radius);
54626 path.arcTo(x + width, y + height, x, y + height, radius);
54627 path.arcTo(x, y + height, x, y, radius);
54628 path.arcTo(x, y, x + radius, y, radius);
54629 }
54630 }
54631 });
54632
54633 /**
54634 * @class Ext.draw.sprite.Image
54635 * @extends Ext.draw.sprite.Rect
54636 *
54637 * A sprite that represents an image.
54638 */
54639 Ext.define("Ext.draw.sprite.Image", {
54640 extend: Ext.draw.sprite.Rect ,
54641 alias: 'sprite.image',
54642 type: 'image',
54643 statics: {
54644 imageLoaders: {}
54645 },
54646
54647 inheritableStatics: {
54648 def: {
54649 processors: {
54650 /**
54651 * @cfg {String} [src=''] The image source of the sprite.
54652 */
54653 src: 'string'
54654 },
54655 defaults: {
54656 src: '',
54657 width: null,
54658 height: null
54659 }
54660 }
54661 },
54662
54663 render: function (surface, ctx) {
54664 var me = this,
54665 attr = me.attr,
54666 mat = attr.matrix,
54667 src = attr.src,
54668 x = attr.x,
54669 y = attr.y,
54670 width = attr.width,
54671 height = attr.height,
54672 loadingStub = Ext.draw.sprite.Image.imageLoaders[src],
54673 imageLoader,
54674 i;
54675
54676 if (loadingStub && loadingStub.done) {
54677 mat.toContext(ctx);
54678 ctx.drawImage(loadingStub.image, x, y, width || loadingStub.width, height || loadingStub.width);
54679 } else if (!loadingStub) {
54680 imageLoader = new Image();
54681 loadingStub = Ext.draw.sprite.Image.imageLoaders[src] = {
54682 image: imageLoader,
54683 done: false,
54684 pendingSprites: [me],
54685 pendingSurfaces: [surface]
54686 };
54687 imageLoader.width = width;
54688 imageLoader.height = height;
54689 imageLoader.onload = function () {
54690 if (!loadingStub.done) {
54691 loadingStub.done = true;
54692 for (i = 0; i < loadingStub.pendingSprites.length; i++) {
54693 loadingStub.pendingSprites[i].setDirty(true);
54694 }
54695 for (i in loadingStub.pendingSurfaces) {
54696 loadingStub.pendingSurfaces[i].renderFrame();
54697 }
54698 }
54699 };
54700 imageLoader.src = src;
54701 } else {
54702 Ext.Array.include(loadingStub.pendingSprites, me);
54703 Ext.Array.include(loadingStub.pendingSurfaces, surface);
54704 }
54705 }
54706 });
54707
54708 /**
54709 * @class Ext.draw.sprite.Instancing
54710 * @extends Ext.draw.sprite.Sprite
54711 *
54712 * Sprite that represents multiple instances based on the given template.
54713 */
54714 Ext.define("Ext.draw.sprite.Instancing", {
54715 extend: Ext.draw.sprite.Sprite ,
54716 alias: 'sprite.instancing',
54717 type: 'instancing',
54718 config: {
54719
54720 /**
54721 * @cfg {Object} [template=null] The sprite template used by all instances.
54722 */
54723 template: null
54724 },
54725 instances: null,
54726 constructor: function (config) {
54727 this.instances = [];
54728 this.callSuper([config]);
54729 if (config && config.template) {
54730 this.setTemplate(config.template);
54731 }
54732 },
54733
54734 applyTemplate: function (template) {
54735 if (!(template instanceof Ext.draw.sprite.Sprite)) {
54736 template = Ext.create(template.xclass || "sprite." + template.type, template);
54737 }
54738 template.setParent(this);
54739 template.attr.children = [];
54740 this.instances = [];
54741 this.position = 0;
54742 return template;
54743 },
54744
54745 /**
54746 * Creates a new sprite instance.
54747 *
54748 * @param {Object} config The configuration of the instance.
54749 * @param {Object} [data]
54750 * @param {Boolean} [bypassNormalization] 'true' to bypass attribute normalization.
54751 * @param {Boolean} [avoidCopy] 'true' to avoid copying.
54752 * @return {Object} The attributes of the instance.
54753 */
54754 createInstance: function (config, data, bypassNormalization, avoidCopy) {
54755 var template = this.getTemplate(),
54756 originalAttr = template.attr,
54757 attr = Ext.Object.chain(originalAttr);
54758 template.topModifier.prepareAttributes(attr);
54759 template.attr = attr;
54760 template.setAttributes(config, bypassNormalization, avoidCopy);
54761 attr.data = data;
54762 this.instances.push(attr);
54763 template.attr = originalAttr;
54764 this.position++;
54765 originalAttr.children.push(attr);
54766 return attr;
54767 },
54768
54769 /**
54770 * Not supported.
54771 *
54772 * @return {null}
54773 */
54774 getBBox: function () { return null; },
54775
54776 /**
54777 * Returns the bounding box for the instance at the given index.
54778 *
54779 * @param {Number} index The index of the instance.
54780 * @param {Boolean} [isWithoutTransform] 'true' to not apply sprite transforms to the bounding box.
54781 * @return {Object} The bounding box for the instance.
54782 */
54783 getBBoxFor: function (index, isWithoutTransform) {
54784 var template = this.getTemplate(),
54785 originalAttr = template.attr,
54786 bbox;
54787 template.attr = this.instances[index];
54788 bbox = template.getBBox(isWithoutTransform);
54789 template.attr = originalAttr;
54790 return bbox;
54791 },
54792
54793 render: function (surface, ctx, clipRegion, region) {
54794 var me = this,
54795 mat = me.attr.matrix,
54796 template = me.getTemplate(),
54797 originalAttr = template.attr,
54798 instances = me.instances,
54799 i, ln = me.position;
54800
54801 mat.toContext(ctx);
54802 template.preRender(surface, ctx, clipRegion, region);
54803 template.useAttributes(ctx, region);
54804 for (i = 0; i < ln; i++) {
54805 if (instances[i].dirtyZIndex) {
54806 break;
54807 }
54808 }
54809 for (i = 0; i < ln; i++) {
54810 if (instances[i].hidden) {
54811 continue;
54812 }
54813 ctx.save();
54814 template.attr = instances[i];
54815 template.applyTransformations();
54816 template.useAttributes(ctx, region);
54817 template.render(surface, ctx, clipRegion, region);
54818 ctx.restore();
54819 }
54820 template.attr = originalAttr;
54821 },
54822
54823 /**
54824 * Sets the attributes for the instance at the given index.
54825 *
54826 * @param {Number} index the index of the instance
54827 * @param {Object} changes the attributes to change
54828 * @param {Boolean} [bypassNormalization] 'true' to avoid attribute normalization
54829 */
54830 setAttributesFor: function (index, changes, bypassNormalization) {
54831 var template = this.getTemplate(),
54832 originalAttr = template.attr,
54833 attr = this.instances[index];
54834 template.attr = attr;
54835 try {
54836 if (bypassNormalization) {
54837 changes = Ext.apply({}, changes);
54838 } else {
54839 changes = template.self.def.normalize(changes);
54840 }
54841 template.topModifier.pushDown(attr, changes);
54842 template.updateDirtyFlags(attr);
54843 } finally {
54844 template.attr = originalAttr;
54845 }
54846 },
54847
54848 destroy: function () {
54849 this.callSuper();
54850 this.instances.length = 0;
54851 this.instances = null;
54852 if (this.getTemplate()) {
54853 this.getTemplate().destroy();
54854 }
54855 }
54856 });
54857
54858 /**
54859 * Linear gradient.
54860 *
54861 * @example preview miniphone
54862 * var component = new Ext.draw.Component({
54863 * items: [{
54864 * type: 'circle',
54865 * cx: 50,
54866 * cy: 50,
54867 * r: 100,
54868 * fillStyle: {
54869 * type: 'linear',
54870 * degrees: 0,
54871 * stops: [
54872 * {
54873 * offset: 0,
54874 * color: 'white'
54875 * },
54876 * {
54877 * offset: 1,
54878 * color: 'blue'
54879 * }
54880 * ]
54881 * }
54882 * }]
54883 * });
54884 * Ext.Viewport.setLayout('fit');
54885 * Ext.Viewport.add(component);
54886 */
54887
54888 Ext.define("Ext.draw.gradient.Linear", {
54889 extend: Ext.draw.gradient.Gradient ,
54890 type: 'linear',
54891 config: {
54892 /**
54893 * @cfg {Number} The degree of rotation of the gradient.
54894 */
54895 degrees: 0
54896 },
54897
54898 setAngle: function (angle) {
54899 this.setDegrees(angle);
54900 },
54901
54902 /**
54903 * @inheritdoc
54904 */
54905 generateGradient: function (ctx, bbox) {
54906 var angle = Ext.draw.Draw.rad(this.getDegrees()),
54907 cos = Math.cos(angle),
54908 sin = Math.sin(angle),
54909 w = bbox.width,
54910 h = bbox.height,
54911 cx = bbox.x + w * 0.5,
54912 cy = bbox.y + h * 0.5,
54913 stops = this.getStops(),
54914 ln = stops.length,
54915 gradient,
54916 l, i;
54917
54918 if (!isNaN(cx) && !isNaN(cy) && h > 0 && w > 0) {
54919 l = (Math.sqrt(h * h + w * w) * Math.abs(Math.cos(angle - Math.atan(h / w)))) / 2;
54920 gradient = ctx.createLinearGradient(
54921 cx + cos * l, cy + sin * l,
54922 cx - cos * l, cy - sin * l
54923 );
54924
54925 for (i = 0; i < ln; i++) {
54926 gradient.addColorStop(stops[i].offset, stops[i].color);
54927 }
54928 return gradient;
54929 }
54930 return 'none';
54931 }
54932 });
54933
54934 /**
54935 * Radial gradient.
54936 *
54937 * @example preview miniphone
54938 * var component = new Ext.draw.Component({
54939 * items: [{
54940 * type: 'circle',
54941 * cx: 50,
54942 * cy: 50,
54943 * r: 100,
54944 * fillStyle: {
54945 * type: 'radial',
54946 * start: {
54947 * x: 0,
54948 * y: 0,
54949 * r: 0
54950 * },
54951 * end: {
54952 * x: 0,
54953 * y: 0,
54954 * r: 1
54955 * },
54956 * stops: [
54957 * {
54958 * offset: 0,
54959 * color: 'white'
54960 * },
54961 * {
54962 * offset: 1,
54963 * color: 'blue'
54964 * }
54965 * ]
54966 * }
54967 * }]
54968 * });
54969 * Ext.Viewport.setLayout('fit');
54970 * Ext.Viewport.add(component);
54971 */
54972 Ext.define("Ext.draw.gradient.Radial", {
54973 extend: Ext.draw.gradient.Gradient ,
54974 type: 'radial',
54975 config: {
54976 /**
54977 * @cfg {Object} start The starting circle of the gradient.
54978 */
54979 start: {
54980 x: 0,
54981 y: 0,
54982 r: 0
54983 },
54984 /**
54985 * @cfg {Object} end The ending circle of the gradient.
54986 */
54987 end: {
54988 x: 0,
54989 y: 0,
54990 r: 1
54991 }
54992 },
54993
54994 applyStart: function (newStart, oldStart) {
54995 if (!oldStart) {
54996 return newStart;
54997 }
54998 var circle = {
54999 x: oldStart.x,
55000 y: oldStart.y,
55001 r: oldStart.r
55002 };
55003
55004 if ('x' in newStart) {
55005 circle.x = newStart.x;
55006 } else if ('centerX' in newStart) {
55007 circle.x = newStart.centerX;
55008 }
55009
55010 if ('y' in newStart) {
55011 circle.y = newStart.y;
55012 } else if ('centerY' in newStart) {
55013 circle.y = newStart.centerY;
55014 }
55015
55016 if ('r' in newStart) {
55017 circle.r = newStart.r;
55018 } else if ('radius' in newStart) {
55019 circle.r = newStart.radius;
55020 }
55021 return circle;
55022 },
55023
55024 applyEnd: function (newEnd, oldEnd) {
55025 if (!oldEnd) {
55026 return newEnd;
55027 }
55028 var circle = {
55029 x: oldEnd.x,
55030 y: oldEnd.y,
55031 r: oldEnd.r
55032 };
55033
55034 if ('x' in newEnd) {
55035 circle.x = newEnd.x;
55036 } else if ('centerX' in newEnd) {
55037 circle.x = newEnd.centerX;
55038 }
55039
55040 if ('y' in newEnd) {
55041 circle.y = newEnd.y;
55042 } else if ('centerY' in newEnd) {
55043 circle.y = newEnd.centerY;
55044 }
55045
55046 if ('r' in newEnd) {
55047 circle.r = newEnd.r;
55048 } else if ('radius' in newEnd) {
55049 circle.r = newEnd.radius;
55050 }
55051 return circle;
55052 },
55053
55054 /**
55055 * @inheritdoc
55056 */
55057 generateGradient: function (ctx, bbox) {
55058 var start = this.getStart(),
55059 end = this.getEnd(),
55060 w = bbox.width * 0.5,
55061 h = bbox.height * 0.5,
55062 x = bbox.x + w,
55063 y = bbox.y + h,
55064 gradient = ctx.createRadialGradient(
55065 x + start.x * w, y + start.y * h, start.r * Math.max(w, h),
55066 x + end.x * w, y + end.y * h, end.r * Math.max(w, h)
55067 ),
55068 stops = this.getStops(),
55069 ln = stops.length,
55070 i;
55071
55072 for (i = 0; i < ln; i++) {
55073 gradient.addColorStop(stops[i].offset, stops[i].color);
55074 }
55075 return gradient;
55076 }
55077 });
55078
55079 /**
55080 * Utility class to calculate [affine transformation](http://en.wikipedia.org/wiki/Affine_transformation) matrix.
55081 *
55082 * This class is compatible with SVGMatrix except:
55083 *
55084 * 1. Ext.draw.Matrix is not read only.
55085 * 2. Using Number as its components rather than floats.
55086 *
55087 * Using this class to reduce the severe numeric problem with HTML Canvas and SVG transformation.
55088 *
55089 */
55090 Ext.define('Ext.draw.Matrix', {
55091
55092 statics: {
55093 /**
55094 * @static
55095 * Return the affine matrix that transform two points (x0, y0) and (x1, y1) to (x0p, y0p) and (x1p, y1p)
55096 * @param {Number} x0
55097 * @param {Number} y0
55098 * @param {Number} x1
55099 * @param {Number} y1
55100 * @param {Number} x0p
55101 * @param {Number} y0p
55102 * @param {Number} x1p
55103 * @param {Number} y1p
55104 */
55105 createAffineMatrixFromTwoPair: function (x0, y0, x1, y1, x0p, y0p, x1p, y1p) {
55106 var dx = x1 - x0,
55107 dy = y1 - y0,
55108 dxp = x1p - x0p,
55109 dyp = y1p - y0p,
55110 r = 1 / (dx * dx + dy * dy),
55111 a = dx * dxp + dy * dyp,
55112 b = dxp * dy - dx * dyp,
55113 c = -a * x0 - b * y0,
55114 f = b * x0 - a * y0;
55115
55116 return new this(a * r, -b * r, b * r, a * r, c * r + x0p, f * r + y0p);
55117 },
55118
55119 /**
55120 * @static
55121 * Return the affine matrix that transform two points (x0, y0) and (x1, y1) to (x0p, y0p) and (x1p, y1p)
55122 * @param {Number} x0
55123 * @param {Number} y0
55124 * @param {Number} x1
55125 * @param {Number} y1
55126 * @param {Number} x0p
55127 * @param {Number} y0p
55128 * @param {Number} x1p
55129 * @param {Number} y1p
55130 */
55131 createPanZoomFromTwoPair: function (x0, y0, x1, y1, x0p, y0p, x1p, y1p) {
55132 if (arguments.length === 2) {
55133 return this.createPanZoomFromTwoPair.apply(this, x0.concat(y0));
55134 }
55135 var dx = x1 - x0,
55136 dy = y1 - y0,
55137 cx = (x0 + x1) * 0.5,
55138 cy = (y0 + y1) * 0.5,
55139 dxp = x1p - x0p,
55140 dyp = y1p - y0p,
55141 cxp = (x0p + x1p) * 0.5,
55142 cyp = (y0p + y1p) * 0.5,
55143 r = dx * dx + dy * dy,
55144 rp = dxp * dxp + dyp * dyp,
55145 scale = Math.sqrt(rp / r);
55146
55147 return new this(scale, 0, 0, scale, cxp - scale * cx, cyp - scale * cy);
55148 },
55149
55150 /**
55151 * @static
55152 * Create a flyweight to wrap the given array.
55153 * The flyweight will directly refer the object and the elements can be changed by other methods.
55154 *
55155 * Do not hold the instance of flyweight matrix.
55156 *
55157 * @param {Array} elements
55158 * @return {Ext.draw.Matrix}
55159 */
55160 fly: (function () {
55161 var flyMatrix = null,
55162 simplefly = function (elements) {
55163 flyMatrix.elements = elements;
55164 return flyMatrix;
55165 };
55166
55167 return function (elements) {
55168 if (!flyMatrix) {
55169 flyMatrix = new Ext.draw.Matrix();
55170 }
55171 flyMatrix.elements = elements;
55172 Ext.draw.Matrix.fly = simplefly;
55173 return flyMatrix;
55174 };
55175 })(),
55176
55177 /**
55178 * @static
55179 * Create a matrix from `mat`. If `mat` is already a matrix, returns it.
55180 * @param {Mixed} mat
55181 * @return {Ext.draw.Matrix}
55182 */
55183 create: function (mat) {
55184 if (mat instanceof this) {
55185 return mat;
55186 }
55187 return new this(mat);
55188 }
55189 },
55190
55191 /**
55192 * Create an affine transform matrix.
55193 *
55194 * @param {Number} xx Coefficient from x to x
55195 * @param {Number} xy Coefficient from x to y
55196 * @param {Number} yx Coefficient from y to x
55197 * @param {Number} yy Coefficient from y to y
55198 * @param {Number} dx Offset of x
55199 * @param {Number} dy Offset of y
55200 */
55201 constructor: function (xx, xy, yx, yy, dx, dy) {
55202 if (xx && xx.length === 6) {
55203 this.elements = xx.slice();
55204 } else if (xx !== undefined) {
55205 this.elements = [xx, xy, yx, yy, dx, dy];
55206 } else {
55207 this.elements = [1, 0, 0, 1, 0, 0];
55208 }
55209 },
55210
55211 /**
55212 * Prepend a matrix onto the current.
55213 *
55214 * __Note:__ The given transform will come after the current one.
55215 *
55216 * @param {Number} xx Coefficient from x to x.
55217 * @param {Number} xy Coefficient from x to y.
55218 * @param {Number} yx Coefficient from y to x.
55219 * @param {Number} yy Coefficient from y to y.
55220 * @param {Number} dx Offset of x.
55221 * @param {Number} dy Offset of y.
55222 * @return {Ext.draw.Matrix} this
55223 */
55224 prepend: function (xx, xy, yx, yy, dx, dy) {
55225 var elements = this.elements,
55226 xx0 = elements[0],
55227 xy0 = elements[1],
55228 yx0 = elements[2],
55229 yy0 = elements[3],
55230 dx0 = elements[4],
55231 dy0 = elements[5];
55232
55233 elements[0] = xx * xx0 + yx * xy0;
55234 elements[1] = xy * xx0 + yy * xy0;
55235 elements[2] = xx * yx0 + yx * yy0;
55236 elements[3] = xy * yx0 + yy * yy0;
55237 elements[4] = xx * dx0 + yx * dy0 + dx;
55238 elements[5] = xy * dx0 + yy * dy0 + dy;
55239 return this;
55240 },
55241
55242 /**
55243 * Prepend a matrix onto the current.
55244 *
55245 * __Note:__ The given transform will come after the current one.
55246 * @param {Ext.draw.Matrix} matrix
55247 * @return {Ext.draw.Matrix} this
55248 */
55249 prependMatrix: function (matrix) {
55250 return this.prepend.apply(this, matrix.elements);
55251 },
55252
55253 /**
55254 * Postpend a matrix onto the current.
55255 *
55256 * __Note:__ The given transform will come before the current one.
55257 *
55258 * @param {Number} xx Coefficient from x to x.
55259 * @param {Number} xy Coefficient from x to y.
55260 * @param {Number} yx Coefficient from y to x.
55261 * @param {Number} yy Coefficient from y to y.
55262 * @param {Number} dx Offset of x.
55263 * @param {Number} dy Offset of y.
55264 * @return {Ext.draw.Matrix} this
55265 */
55266 append: function (xx, xy, yx, yy, dx, dy) {
55267 var elements = this.elements,
55268 xx0 = elements[0],
55269 xy0 = elements[1],
55270 yx0 = elements[2],
55271 yy0 = elements[3],
55272 dx0 = elements[4],
55273 dy0 = elements[5];
55274
55275 elements[0] = xx * xx0 + xy * yx0;
55276 elements[1] = xx * xy0 + xy * yy0;
55277 elements[2] = yx * xx0 + yy * yx0;
55278 elements[3] = yx * xy0 + yy * yy0;
55279 elements[4] = dx * xx0 + dy * yx0 + dx0;
55280 elements[5] = dx * xy0 + dy * yy0 + dy0;
55281 return this;
55282 },
55283
55284 /**
55285 * Postpend a matrix onto the current.
55286 *
55287 * __Note:__ The given transform will come before the current one.
55288 *
55289 * @param {Ext.draw.Matrix} matrix
55290 * @return {Ext.draw.Matrix} this
55291 */
55292 appendMatrix: function (matrix) {
55293 return this.append.apply(this, matrix.elements);
55294 },
55295
55296 /**
55297 * Set the elements of a Matrix
55298 * @param {Number} xx
55299 * @param {Number} xy
55300 * @param {Number} yx
55301 * @param {Number} yy
55302 * @param {Number} dx
55303 * @param {Number} dy
55304 * @return {Ext.draw.Matrix} this
55305 */
55306 set: function (xx, xy, yx, yy, dx, dy) {
55307 var elements = this.elements;
55308
55309 elements[0] = xx;
55310 elements[1] = xy;
55311 elements[2] = yx;
55312 elements[3] = yy;
55313 elements[4] = dx;
55314 elements[5] = dy;
55315 return this;
55316 },
55317
55318 /**
55319 * Return a new matrix represents the opposite transformation of the current one.
55320 *
55321 * @param {Ext.draw.Matrix} [target] A target matrix. If present, it will receive
55322 * the result of inversion to avoid creating a new object.
55323 *
55324 * @return {Ext.draw.Matrix}
55325 */
55326 inverse: function (target) {
55327 var elements = this.elements,
55328 a = elements[0],
55329 b = elements[1],
55330 c = elements[2],
55331 d = elements[3],
55332 e = elements[4],
55333 f = elements[5],
55334 rDim = 1 / (a * d - b * c);
55335
55336 a *= rDim;
55337 b *= rDim;
55338 c *= rDim;
55339 d *= rDim;
55340 if (target) {
55341 target.set(d, -b, -c, a, c * f - d * e, b * e - a * f);
55342 return target;
55343 } else {
55344 return new Ext.draw.Matrix(d, -b, -c, a, c * f - d * e, b * e - a * f);
55345 }
55346 },
55347
55348 /**
55349 * Translate the matrix.
55350 *
55351 * @param {Number} x
55352 * @param {Number} y
55353 * @param {Boolean} [prepend] If `true`, this will transformation be prepended to the matrix.
55354 * @return {Ext.draw.Matrix} this
55355 */
55356 translate: function (x, y, prepend) {
55357 if (prepend) {
55358 return this.prepend(1, 0, 0, 1, x, y);
55359 } else {
55360 return this.append(1, 0, 0, 1, x, y);
55361 }
55362 },
55363
55364 /**
55365 * Scale the matrix.
55366 *
55367 * @param {Number} sx
55368 * @param {Number} sy
55369 * @param {Number} scx
55370 * @param {Number} scy
55371 * @param {Boolean} [prepend] If `true`, this will transformation be prepended to the matrix.
55372 * @return {Ext.draw.Matrix} this
55373 */
55374 scale: function (sx, sy, scx, scy, prepend) {
55375 var me = this;
55376
55377 // null or undefined
55378 if (sy == null) {
55379 sy = sx;
55380 }
55381 if (scx === undefined) {
55382 scx = 0;
55383 }
55384 if (scy === undefined) {
55385 scy = 0;
55386 }
55387
55388 if (prepend) {
55389 return me.prepend(sx, 0, 0, sy, scx - scx * sx, scy - scy * sy);
55390 } else {
55391 return me.append(sx, 0, 0, sy, scx - scx * sx, scy - scy * sy);
55392 }
55393 },
55394
55395 /**
55396 * Rotate the matrix.
55397 *
55398 * @param {Number} angle Radians to rotate
55399 * @param {Number|null} rcx Center of rotation.
55400 * @param {Number|null} rcy Center of rotation.
55401 * @param {Boolean} [prepend] If `true`, this will transformation be prepended to the matrix.
55402 * @return {Ext.draw.Matrix} this
55403 */
55404 rotate: function (angle, rcx, rcy, prepend) {
55405 var me = this,
55406 cos = Math.cos(angle),
55407 sin = Math.sin(angle);
55408
55409 rcx = rcx || 0;
55410 rcy = rcy || 0;
55411
55412 if (prepend) {
55413 return me.prepend(
55414 cos, sin,
55415 -sin, cos,
55416 rcx - cos * rcx + rcy * sin,
55417 rcy - cos * rcy - rcx * sin
55418 );
55419 } else {
55420 return me.append(
55421 cos, sin,
55422 -sin, cos,
55423 rcx - cos * rcx + rcy * sin,
55424 rcy - cos * rcy - rcx * sin
55425 );
55426 }
55427 },
55428
55429 /**
55430 * Rotate the matrix by the angle of a vector.
55431 *
55432 * @param {Number} x
55433 * @param {Number} y
55434 * @param {Boolean} [prepend] If `true`, this will transformation be prepended to the matrix.
55435 * @return {Ext.draw.Matrix} this
55436 */
55437 rotateFromVector: function (x, y, prepend) {
55438 var me = this,
55439 d = Math.sqrt(x * x + y * y),
55440 cos = x / d,
55441 sin = y / d;
55442 if (prepend) {
55443 return me.prepend(cos, sin, -sin, cos, 0, 0);
55444 } else {
55445 return me.append(cos, sin, -sin, cos, 0, 0);
55446 }
55447 },
55448
55449 /**
55450 * Clone this matrix.
55451 * @return {Ext.draw.Matrix}
55452 */
55453 clone: function () {
55454 return new Ext.draw.Matrix(this.elements);
55455 },
55456
55457 /**
55458 * Horizontally flip the matrix
55459 * @return {Ext.draw.Matrix} this
55460 */
55461 flipX: function () {
55462 return this.append(-1, 0, 0, 1, 0, 0);
55463 },
55464
55465 /**
55466 * Vertically flip the matrix
55467 * @return {Ext.draw.Matrix} this
55468 */
55469 flipY: function () {
55470 return this.append(1, 0, 0, -1, 0, 0);
55471 },
55472
55473 /**
55474 * Skew the matrix
55475 * @param {Number} angle
55476 * @return {Ext.draw.Matrix} this
55477 */
55478 skewX: function (angle) {
55479 return this.append(1, Math.tan(angle), 0, -1, 0, 0);
55480 },
55481
55482 /**
55483 * Skew the matrix
55484 * @param {Number} angle
55485 * @return {Ext.draw.Matrix} this
55486 */
55487 skewY: function (angle) {
55488 return this.append(1, 0, Math.tan(angle), -1, 0, 0);
55489 },
55490
55491 /**
55492 * Reset the matrix to identical.
55493 * @return {Ext.draw.Matrix} this
55494 */
55495 reset: function () {
55496 return this.set(1, 0, 0, 1, 0, 0);
55497 },
55498
55499 /**
55500 * @private
55501 * Split Matrix to `{{devicePixelRatio,c,0},{b,devicePixelRatio,0},{0,0,1}}.{{xx,0,dx},{0,yy,dy},{0,0,1}}`
55502 * @return {Object} Object with b,c,d=devicePixelRatio,xx,yy,dx,dy
55503 */
55504 precisionCompensate: function (devicePixelRatio, comp) {
55505 var elements = this.elements,
55506 x2x = elements[0],
55507 x2y = elements[1],
55508 y2x = elements[2],
55509 y2y = elements[3],
55510 newDx = elements[4],
55511 newDy = elements[5],
55512 r = x2y * y2x - x2x * y2y;
55513
55514 comp.b = devicePixelRatio * x2y / x2x;
55515 comp.c = devicePixelRatio * y2x / y2y;
55516 comp.d = devicePixelRatio;
55517 comp.xx = x2x / devicePixelRatio;
55518 comp.yy = y2y / devicePixelRatio;
55519 comp.dx = (newDy * x2x * y2x - newDx * x2x * y2y) / r / devicePixelRatio;
55520 comp.dy = (newDx * x2y * y2y - newDy * x2x * y2y) / r / devicePixelRatio;
55521 },
55522
55523 /**
55524 * @private
55525 * Split Matrix to `{{1,c,0},{b,d,0},{0,0,1}}.{{xx,0,dx},{0,xx,dy},{0,0,1}}`
55526 * @return {Object} Object with b,c,d,xx,yy=xx,dx,dy
55527 */
55528 precisionCompensateRect: function (devicePixelRatio, comp) {
55529 var elements = this.elements,
55530 x2x = elements[0],
55531 x2y = elements[1],
55532 y2x = elements[2],
55533 y2y = elements[3],
55534 newDx = elements[4],
55535 newDy = elements[5],
55536 yxOnXx = y2x / x2x;
55537
55538 comp.b = devicePixelRatio * x2y / x2x;
55539 comp.c = devicePixelRatio * yxOnXx;
55540 comp.d = devicePixelRatio * y2y / x2x;
55541 comp.xx = x2x / devicePixelRatio;
55542 comp.yy = x2x / devicePixelRatio;
55543 comp.dx = (newDy * y2x - newDx * y2y) / (x2y * yxOnXx - y2y) / devicePixelRatio;
55544 comp.dy = -(newDy * x2x - newDx * x2y) / (x2y * yxOnXx - y2y) / devicePixelRatio;
55545 },
55546
55547 /**
55548 * Transform point returning the x component of the result.
55549 * @param {Number} x
55550 * @param {Number} y
55551 * @return {Number} x component of the result.
55552 */
55553 x: function (x, y) {
55554 var elements = this.elements;
55555 return x * elements[0] + y * elements[2] + elements[4];
55556 },
55557
55558 /**
55559 * Transform point returning the y component of the result.
55560 * @param {Number} x
55561 * @param {Number} y
55562 * @return {Number} y component of the result.
55563 */
55564 y: function (x, y) {
55565 var elements = this.elements;
55566
55567 return x * elements[1] + y * elements[3] + elements[5];
55568 },
55569
55570 /**
55571 * @private
55572 * @param {Number} i
55573 * @param {Number} j
55574 * @return {String}
55575 */
55576 get: function (i, j) {
55577 return +this.elements[i + j * 2].toFixed(4);
55578 },
55579
55580 /**
55581 * Transform a point to a new array.
55582 * @param {Array} point
55583 * @return {Array}
55584 */
55585 transformPoint: function (point) {
55586 var elements = this.elements;
55587
55588 return [
55589 point[0] * elements[0] + point[1] * elements[2] + elements[4],
55590 point[0] * elements[1] + point[1] * elements[3] + elements[5]
55591 ];
55592 },
55593
55594 /**
55595 * @param {Object} bbox Given as `{x: Number, y: Number, width: Number, height: Number}`.
55596 * @param {Number} [radius]
55597 * @param {Object} [target] Optional target object to recieve the result.
55598 * Recommended to use it for better gc.
55599 *
55600 * @return {Object} Object with x, y, width and height.
55601 */
55602 transformBBox: function (bbox, radius, target) {
55603 var elements = this.elements,
55604 l = bbox.x,
55605 t = bbox.y,
55606 w0 = bbox.width * 0.5,
55607 h0 = bbox.height * 0.5,
55608 xx = elements[0],
55609 xy = elements[1],
55610 yx = elements[2],
55611 yy = elements[3],
55612 cx = l + w0,
55613 cy = t + h0,
55614 w, h, scales;
55615
55616 if (radius) {
55617 w0 -= radius;
55618 h0 -= radius;
55619 scales = [
55620 Math.sqrt(elements[0] * elements[0] + elements[2] * elements[2]),
55621 Math.sqrt(elements[1] * elements[1] + elements[3] * elements[3])
55622 ];
55623 w = Math.abs(w0 * xx) + Math.abs(h0 * yx) + Math.abs(scales[0] * radius);
55624 h = Math.abs(w0 * xy) + Math.abs(h0 * yy) + Math.abs(scales[1] * radius);
55625 } else {
55626 w = Math.abs(w0 * xx) + Math.abs(h0 * yx);
55627 h = Math.abs(w0 * xy) + Math.abs(h0 * yy);
55628 }
55629
55630 if (!target) {
55631 target = {};
55632 }
55633
55634 target.x = cx * xx + cy * yx + elements[4] - w;
55635 target.y = cx * xy + cy * yy + elements[5] - h;
55636 target.width = w + w;
55637 target.height = h + h;
55638
55639 return target;
55640 },
55641
55642 /**
55643 * Transform a list for points.
55644 *
55645 * __Note:__ will change the original list but not points inside it.
55646 * @param {Array} list
55647 * @return {Array} list
55648 */
55649 transformList: function (list) {
55650 var elements = this.elements,
55651 xx = elements[0], yx = elements[2], dx = elements[4],
55652 xy = elements[1], yy = elements[3], dy = elements[5],
55653 ln = list.length,
55654 p, i;
55655
55656 for (i = 0; i < ln; i++) {
55657 p = list[i];
55658 list[i] = [
55659 p[0] * xx + p[1] * yx + dx,
55660 p[0] * xy + p[1] * yy + dy
55661 ];
55662 }
55663 return list;
55664 },
55665
55666 /**
55667 * Determines whether this matrix is an identity matrix (no transform).
55668 * @return {Boolean}
55669 */
55670 isIdentity: function () {
55671 var elements = this.elements;
55672
55673 return elements[0] === 1 &&
55674 elements[1] === 0 &&
55675 elements[2] === 0 &&
55676 elements[3] === 1 &&
55677 elements[4] === 0 &&
55678 elements[5] === 0;
55679 },
55680
55681 /**
55682 * Determines if this matrix has the same values as another matrix.
55683 * @param {Ext.draw.Matrix} matrix
55684 * @return {Boolean}
55685 */
55686 equals: function (matrix) {
55687 var elements = this.elements,
55688 elements2 = matrix.elements;
55689
55690 return elements[0] === elements2[0] &&
55691 elements[1] === elements2[1] &&
55692 elements[2] === elements2[2] &&
55693 elements[3] === elements2[3] &&
55694 elements[4] === elements2[4] &&
55695 elements[5] === elements2[5];
55696 },
55697
55698 /**
55699 * Create an array of elements by horizontal order (xx,yx,dx,yx,yy,dy).
55700 * @return {Array}
55701 */
55702 toArray: function () {
55703 var elements = this.elements;
55704 return [elements[0], elements[2], elements[4], elements[1], elements[3], elements[5]];
55705 },
55706
55707 /**
55708 * Create an array of elements by vertical order (xx,xy,yx,yy,dx,dy).
55709 * @return {Array|String}
55710 */
55711 toVerticalArray: function () {
55712 return this.elements.slice();
55713 },
55714
55715 /**
55716 * Get an array of elements.
55717 * The numbers are rounded to keep only 4 decimals.
55718 * @return {Array}
55719 */
55720 toString: function () {
55721 var me = this;
55722 return [me.get(0, 0), me.get(0, 1), me.get(1, 0), me.get(1, 1), me.get(2, 0), me.get(2, 1)].join(',');
55723 },
55724
55725 /**
55726 * Apply the matrix to a drawing context.
55727 * @param {Object} ctx
55728 * @return {Ext.draw.Matrix} this
55729 */
55730 toContext: function (ctx) {
55731 ctx.transform.apply(ctx, this.elements);
55732 return this;
55733 },
55734
55735 /**
55736 * Return a string that can be used as transform attribute in SVG.
55737 * @return {String}
55738 */
55739 toSvg: function () {
55740 var elements = this.elements;
55741 // The reason why we cannot use `.join` is the `1e5` form is not accepted in svg.
55742 return "matrix(" +
55743 elements[0].toFixed(9) + ',' +
55744 elements[1].toFixed(9) + ',' +
55745 elements[2].toFixed(9) + ',' +
55746 elements[3].toFixed(9) + ',' +
55747 elements[4].toFixed(9) + ',' +
55748 elements[5].toFixed(9) +
55749 ")";
55750 },
55751
55752 /**
55753 * Get the x scale of the matrix.
55754 * @return {Number}
55755 */
55756 getScaleX: function () {
55757 var elements = this.elements;
55758 return Math.sqrt(elements[0] * elements[0] + elements[2] * elements[2]);
55759 },
55760
55761 /**
55762 * Get the y scale of the matrix.
55763 * @return {Number}
55764 */
55765 getScaleY: function () {
55766 var elements = this.elements;
55767 return Math.sqrt(elements[1] * elements[1] + elements[3] * elements[3]);
55768 },
55769
55770 /**
55771 * Get x-to-x component of the matrix
55772 * @return {Number}
55773 */
55774 getXX: function () {
55775 return this.elements[0];
55776 },
55777
55778 /**
55779 * Get x-to-y component of the matrix.
55780 * @return {Number}
55781 */
55782 getXY: function () {
55783 return this.elements[1];
55784 },
55785
55786 /**
55787 * Get y-to-x component of the matrix.
55788 * @return {Number}
55789 */
55790 getYX: function () {
55791 return this.elements[2];
55792 },
55793
55794 /**
55795 * Get y-to-y component of the matrix.
55796 * @return {Number}
55797 */
55798 getYY: function () {
55799 return this.elements[3];
55800 },
55801
55802 /**
55803 * Get offset x component of the matrix.
55804 * @return {Number}
55805 */
55806 getDX: function () {
55807 return this.elements[4];
55808 },
55809
55810 /**
55811 * Get offset y component of the matrix.
55812 * @return {Number}
55813 */
55814 getDY: function () {
55815 return this.elements[5];
55816 },
55817
55818 /**
55819 * Split matrix into Translate, Scale, Shear, and Rotate.
55820 * @return {Object}
55821 */
55822 split: function () {
55823 var el = this.elements,
55824 xx = el[0],
55825 xy = el[1],
55826 yx = el[2],
55827 yy = el[3],
55828 out = {
55829 translateX: el[4],
55830 translateY: el[5]
55831 };
55832 out.scaleX = Math.sqrt(xx * xx + yx * yx);
55833 out.shear = (xx * xy + yx * yy) / out.scaleX;
55834 xy -= out.shear * xx;
55835 yy -= out.shear * yx;
55836 out.scaleY = Math.sqrt(xy * xy + yy * yy);
55837 out.shear /= out.scaleY;
55838 out.rotation = -Math.atan2(yx / out.scaleX, xy / out.scaleY);
55839 out.isSimple = Math.abs(out.shear) < 1e-9 && (!out.rotation || Math.abs(out.scaleX - out.scaleY) < 1e-9);
55840 return out;
55841 }
55842 }, function () {
55843 function registerName(properties, name, i) {
55844 properties[name] = {
55845 get: function () {
55846 return this.elements[i];
55847 },
55848 set: function (val) {
55849 this.elements[i] = val;
55850 }
55851 };
55852 }
55853
55854 // Compatibility with SVGMatrix
55855 // https://developer.mozilla.org/en/DOM/SVGMatrix
55856 if (Object.defineProperties) {
55857 var properties = {};
55858 /**
55859 * @property {Number} a Get x-to-x component of the matrix. Avoid using it for performance consideration.
55860 * Use {@link #getXX} instead.
55861 */
55862 registerName(properties, 'a', 0);
55863
55864 // TODO: Help me finish this.
55865 registerName(properties, 'b', 1);
55866 registerName(properties, 'c', 2);
55867 registerName(properties, 'd', 3);
55868 registerName(properties, 'e', 4);
55869 registerName(properties, 'f', 5);
55870 Object.defineProperties(this.prototype, properties);
55871 }
55872
55873 /**
55874 * Postpend a matrix onto the current.
55875 *
55876 * __Note:__ The given transform will come before the current one.
55877 *
55878 * @method
55879 * @param {Ext.draw.Matrix} matrix
55880 * @return {Ext.draw.Matrix} this
55881 */
55882 this.prototype.multiply = this.prototype.appendMatrix;
55883 });
55884
55885 /**
55886 * A Surface is an interface to render methods inside a draw {@link Ext.draw.Component}.
55887 * A Surface contains methods to render sprites, get bounding boxes of sprites, add
55888 * sprites to the canvas, initialize other graphic components, etc. One of the most used
55889 * methods for this class is the `add` method, to add Sprites to the surface.
55890 *
55891 * Most of the Surface methods are abstract and they have a concrete implementation
55892 * in Canvas or SVG engines.
55893 *
55894 * A Surface instance can be accessed as a property of a draw component. For example:
55895 *
55896 * drawComponent.getSurface('main').add({
55897 * type: 'circle',
55898 * fill: '#ffc',
55899 * radius: 100,
55900 * x: 100,
55901 * y: 100
55902 * });
55903 *
55904 * The configuration object passed in the `add` method is the same as described in the {@link Ext.draw.sprite.Sprite}
55905 * class documentation.
55906 *
55907 * ## Example
55908 *
55909 * drawComponent.getSurface('main').add([
55910 * {
55911 * type: 'circle',
55912 * radius: 10,
55913 * fill: '#f00',
55914 * x: 10,
55915 * y: 10
55916 * },
55917 * {
55918 * type: 'circle',
55919 * radius: 10,
55920 * fill: '#0f0',
55921 * x: 50,
55922 * y: 50
55923 * },
55924 * {
55925 * type: 'circle',
55926 * radius: 10,
55927 * fill: '#00f',
55928 * x: 100,
55929 * y: 100
55930 * },
55931 * {
55932 * type: 'rect',
55933 * radius: 10,
55934 * x: 10,
55935 * y: 10
55936 * },
55937 * {
55938 * type: 'rect',
55939 * radius: 10,
55940 * x: 50,
55941 * y: 50
55942 * },
55943 * {
55944 * type: 'rect',
55945 * radius: 10,
55946 * x: 100,
55947 * y: 100
55948 * }
55949 * ]);
55950 *
55951 */
55952 Ext.define('Ext.draw.Surface', {
55953 extend: Ext.Component ,
55954 xtype: 'surface',
55955
55956
55957
55958
55959
55960
55961
55962
55963
55964
55965
55966
55967
55968 defaultIdPrefix: 'ext-surface-',
55969
55970 /**
55971 * The reported device pixel density.
55972 */
55973 devicePixelRatio: window.devicePixelRatio || 1,
55974
55975 statics: {
55976 /**
55977 * Stably sort the list of sprites by their zIndex.
55978 * TODO: Improve the performance. Reduce gc impact.
55979 * @param {Array} list
55980 */
55981 stableSort: function (list) {
55982 if (list.length < 2) {
55983 return;
55984 }
55985 var keys = {}, sortedKeys, result = [], i, ln, zIndex;
55986
55987 for (i = 0, ln = list.length; i < ln; i++) {
55988 zIndex = list[i].attr.zIndex;
55989 if (!keys[zIndex]) {
55990 keys[zIndex] = [list[i]];
55991 } else {
55992 keys[zIndex].push(list[i]);
55993 }
55994 }
55995 sortedKeys = Ext.Object.getKeys(keys).sort(function (a, b) {return a - b;});
55996 for (i = 0, ln = sortedKeys.length; i < ln; i++) {
55997 result.push.apply(result, keys[sortedKeys[i]]);
55998 }
55999 for (i = 0, ln = list.length; i < ln; i++) {
56000 list[i] = result[i];
56001 }
56002 }
56003 },
56004
56005 config: {
56006 /**
56007 * @cfg {Array}
56008 * The region of the surface related to its component.
56009 */
56010 region: null,
56011
56012 /**
56013 * @cfg {Object}
56014 * Background sprite config of the surface.
56015 */
56016 background: null,
56017
56018 /**
56019 * @cfg {Array}
56020 * Array of sprite instances.
56021 */
56022 items: [],
56023
56024 /**
56025 * @cfg {Boolean}
56026 * Indicates whether the surface needs redraw.
56027 */
56028 dirty: false
56029 },
56030
56031 dirtyPredecessor: 0,
56032
56033 constructor: function (config) {
56034 var me = this;
56035
56036 me.predecessors = [];
56037 me.successors = [];
56038 // The `pendingRenderFrame` flag is used to indicate that `predecessors` (surfaces that should render first)
56039 // are dirty, and to call `renderFrame` when all `predecessors` have their `renderFrame` called
56040 // (i.e. not dirty anymore).
56041 me.pendingRenderFrame = false;
56042 me.map = {};
56043
56044 me.callSuper([config]);
56045 me.matrix = new Ext.draw.Matrix();
56046 me.inverseMatrix = me.matrix.inverse(me.inverseMatrix);
56047 me.resetTransform();
56048 },
56049
56050 /**
56051 * Round the number to align to the pixels on device.
56052 * @param {Number} num The number to align.
56053 * @return {Number} The resultant alignment.
56054 */
56055 roundPixel: function (num) {
56056 return Math.round(this.devicePixelRatio * num) / this.devicePixelRatio;
56057 },
56058
56059 /**
56060 * Mark the surface to render after another surface is updated.
56061 * @param {Ext.draw.Surface} surface The surface to wait for.
56062 */
56063 waitFor: function (surface) {
56064 var me = this,
56065 predecessors = me.predecessors;
56066 if (!Ext.Array.contains(predecessors, surface)) {
56067 predecessors.push(surface);
56068 surface.successors.push(me);
56069 if (surface._dirty) {
56070 me.dirtyPredecessor++;
56071 }
56072 }
56073 },
56074
56075 setDirty: function (dirty) {
56076 if (this._dirty !== dirty) {
56077 var successors = this.successors, successor,
56078 i, ln = successors.length;
56079 for (i = 0; i < ln; i++) {
56080 successor = successors[i];
56081 if (dirty) {
56082 successor.dirtyPredecessor++;
56083 successor.setDirty(true);
56084 } else {
56085 successor.dirtyPredecessor--;
56086 if (successor.dirtyPredecessor === 0 && successor.pendingRenderFrame) {
56087 successor.renderFrame();
56088 }
56089 }
56090 }
56091 this._dirty = dirty;
56092 }
56093 },
56094
56095 applyElement: function (newElement, oldElement) {
56096 if (oldElement) {
56097 oldElement.set(newElement);
56098 } else {
56099 oldElement = Ext.Element.create(newElement);
56100 }
56101 this.setDirty(true);
56102 return oldElement;
56103 },
56104
56105 applyBackground: function (background, oldBackground) {
56106 this.setDirty(true);
56107 if (Ext.isString(background)) {
56108 background = { fillStyle: background };
56109 }
56110 return Ext.factory(background, Ext.draw.sprite.Rect, oldBackground);
56111 },
56112
56113 applyRegion: function (region, oldRegion) {
56114 if (oldRegion && region[0] === oldRegion[0] && region[1] === oldRegion[1] && region[2] === oldRegion[2] && region[3] === oldRegion[3]) {
56115 return;
56116 }
56117 if (Ext.isArray(region)) {
56118 return [region[0], region[1], region[2], region[3]];
56119 } else if (Ext.isObject(region)) {
56120 return [
56121 region.x || region.left,
56122 region.y || region.top,
56123 region.width || (region.right - region.left),
56124 region.height || (region.bottom - region.top)
56125 ];
56126 }
56127 },
56128
56129 updateRegion: function (region) {
56130 var me = this,
56131 l = region[0],
56132 t = region[1],
56133 r = l + region[2],
56134 b = t + region[3],
56135 background = this.getBackground(),
56136 element = me.element;
56137
56138 element.setBox({
56139 top: Math.floor(t),
56140 left: Math.floor(l),
56141 width: Math.ceil(r - Math.floor(l)),
56142 height: Math.ceil(b - Math.floor(t))
56143 });
56144
56145 if (background) {
56146 background.setAttributes({
56147 x: 0,
56148 y: 0,
56149 width: Math.ceil(r - Math.floor(l)),
56150 height: Math.ceil(b - Math.floor(t))
56151 });
56152 }
56153 me.setDirty(true);
56154 },
56155
56156 /**
56157 * Reset the matrix of the surface.
56158 */
56159 resetTransform: function () {
56160 this.matrix.set(1, 0, 0, 1, 0, 0);
56161 this.inverseMatrix.set(1, 0, 0, 1, 0, 0);
56162 this.setDirty(true);
56163 },
56164
56165 updateComponent: function (component, oldComponent) {
56166 if (component) {
56167 component.element.dom.appendChild(this.element.dom);
56168 }
56169 },
56170
56171 /**
56172 * Get the sprite by id or index.
56173 * It will first try to find a sprite with the given id, otherwise will try to use the id as an index.
56174 * @param {String|Number} id
56175 * @returns {Ext.draw.sprite.Sprite}
56176 */
56177 get: function (id) {
56178 return this.map[id] || this.items[id];
56179 },
56180
56181 /**
56182 * Add a Sprite to the surface.
56183 * You can put any number of object as parameter.
56184 * See {@link Ext.draw.sprite.Sprite} for the configuration object to be passed into this method.
56185 *
56186 * For example:
56187 *
56188 * drawComponent.surface.add({
56189 * type: 'circle',
56190 * fill: '#ffc',
56191 * radius: 100,
56192 * x: 100,
56193 * y: 100
56194 * });
56195 *
56196 */
56197 add: function () {
56198 var me = this,
56199 args = Array.prototype.slice.call(arguments),
56200 argIsArray = Ext.isArray(args[0]),
56201 results = [],
56202 sprite, sprites, items, i, ln;
56203
56204 items = Ext.Array.clean(argIsArray ? args[0] : args);
56205 if (!items.length) {
56206 return results;
56207 }
56208 sprites = me.prepareItems(items);
56209
56210 for (i = 0, ln = sprites.length; i < ln; i++) {
56211 sprite = sprites[i];
56212 me.map[sprite.getId()] = sprite;
56213 results.push(sprite);
56214 sprite.setParent(this);
56215 me.onAdd(sprite);
56216 }
56217
56218 items = me.getItems();
56219 if (items) {
56220 items.push.apply(items, results);
56221 }
56222
56223 me.dirtyZIndex = true;
56224 me.setDirty(true);
56225
56226 if (!argIsArray && results.length === 1) {
56227 return results[0];
56228 } else {
56229 return results;
56230 }
56231 },
56232
56233 /**
56234 * @protected
56235 * Invoked when a sprite is added to the surface.
56236 * @param {Ext.draw.sprite.Sprite} sprite The sprite to be added.
56237 */
56238 onAdd: Ext.emptyFn,
56239
56240 /**
56241 * Remove a given sprite from the surface, optionally destroying the sprite in the process.
56242 * You can also call the sprite own `remove` method.
56243 *
56244 * For example:
56245 *
56246 * drawComponent.surface.remove(sprite);
56247 * // or...
56248 * sprite.remove();
56249 *
56250 * @param {Ext.draw.sprite.Sprite} sprite
56251 * @param {Boolean} [destroySprite=false]
56252 */
56253 remove: function (sprite, destroySprite) {
56254 if (sprite) {
56255 delete this.map[sprite.getId()];
56256 if (destroySprite) {
56257 sprite.destroy();
56258 } else {
56259 sprite.setParent(null);
56260 Ext.Array.remove(this.getItems(), sprite);
56261 }
56262 this.dirtyZIndex = true;
56263 this.setDirty(true);
56264 }
56265 },
56266
56267 /**
56268 * Remove all sprites from the surface, optionally destroying the sprites in the process.
56269 *
56270 * For example:
56271 *
56272 * drawComponent.getSurface('main').removeAll();
56273 *
56274 * @param {Boolean} [destroySprites=false]
56275 */
56276 removeAll: function (destroySprites) {
56277 var items = this.getItems(),
56278 i = items.length;
56279 if (destroySprites) {
56280 while (i > 0) {
56281 items[--i].destroy();
56282 }
56283 } else {
56284 while (i > 0) {
56285 items[--i].setParent(null);
56286 }
56287 }
56288 items.length = 0;
56289 this.map = {};
56290 this.dirtyZIndex = true;
56291 },
56292
56293 // @private
56294 applyItems: function (items) {
56295 if (this.getItems()) {
56296 this.removeAll(true);
56297 }
56298 return Ext.Array.from(this.add(items));
56299 },
56300
56301 /**
56302 * @private
56303 * Initialize and apply defaults to surface items.
56304 */
56305 prepareItems: function (items) {
56306 items = [].concat(items);
56307 // Make sure defaults are applied and item is initialized
56308
56309 var me = this,
56310 item, i, ln, j,
56311 removeSprite = function (sprite) {
56312 this.remove(sprite, false);
56313 };
56314
56315 for (i = 0, ln = items.length; i < ln; i++) {
56316 item = items[i];
56317 if (!(item instanceof Ext.draw.sprite.Sprite)) {
56318 // Temporary, just take in configs...
56319 item = items[i] = me.createItem(item);
56320 }
56321 item.on('beforedestroy', removeSprite, me);
56322 }
56323 return items;
56324 },
56325
56326 /**
56327 * @private Creates an item and appends it to the surface. Called
56328 * as an internal method when calling `add`.
56329 */
56330 createItem: function (config) {
56331 var sprite = Ext.create(config.xclass || 'sprite.' + config.type, config);
56332 return sprite;
56333 },
56334
56335 /**
56336 * Return the minimal bounding box that contains all the sprites bounding boxes in the given list of sprites.
56337 * @param {Ext.draw.sprite.Sprite[]|Ext.draw.sprite.Sprite} sprites
56338 * @param {Boolean} [isWithoutTransform=false]
56339 * @returns {{x: Number, y: Number, width: number, height: number}}
56340 */
56341 getBBox: function (sprites, isWithoutTransform) {
56342 var sprites = Ext.Array.from(sprites),
56343 left = Infinity,
56344 right = -Infinity,
56345 top = Infinity,
56346 bottom = -Infinity,
56347 sprite, bbox, i, ln;
56348
56349 for (i = 0, ln = sprites.length; i < ln; i++) {
56350 sprite = sprites[i];
56351 bbox = sprite.getBBox(isWithoutTransform);
56352 if (left > bbox.x) {
56353 left = bbox.x;
56354 }
56355 if (right < bbox.x + bbox.width) {
56356 right = bbox.x + bbox.width;
56357 }
56358 if (top > bbox.y) {
56359 top = bbox.y;
56360 }
56361 if (bottom < bbox.y + bbox.height) {
56362 bottom = bbox.y + bbox.height;
56363 }
56364 }
56365 return {
56366 x: left,
56367 y: top,
56368 width: right - left,
56369 height: bottom - top
56370 };
56371 },
56372
56373 /**
56374 * Empty the surface content (without touching the sprites.)
56375 */
56376 clear: Ext.emptyFn,
56377
56378 /**
56379 * @private
56380 * Order the items by their z-index if any of that has been changed since last sort.
56381 */
56382 orderByZIndex: function () {
56383 var me = this,
56384 items = me.getItems(),
56385 dirtyZIndex = false,
56386 i, ln;
56387
56388 if (me.getDirty()) {
56389 for (i = 0, ln = items.length; i < ln; i++) {
56390 if (items[i].attr.dirtyZIndex) {
56391 dirtyZIndex = true;
56392 break;
56393 }
56394 }
56395 if (dirtyZIndex) {
56396 // sort by zIndex
56397 Ext.draw.Surface.stableSort(items);
56398 this.setDirty(true);
56399 }
56400
56401 for (i = 0, ln = items.length; i < ln; i++) {
56402 items[i].attr.dirtyZIndex = false;
56403 }
56404 }
56405 },
56406
56407 /**
56408 * Force the element to redraw.
56409 */
56410 repaint: function () {
56411 var me = this;
56412 me.repaint = Ext.emptyFn;
56413 setTimeout(function () {
56414 delete me.repaint;
56415 me.element.repaint();
56416 }, 1);
56417 },
56418
56419 /**
56420 * Triggers the re-rendering of the canvas.
56421 */
56422 renderFrame: function () {
56423 if (!this.element) {
56424 return;
56425 }
56426 if (this.dirtyPredecessor > 0) {
56427 this.pendingRenderFrame = true;
56428 return;
56429 }
56430
56431 var me = this,
56432 region = this.getRegion(),
56433 background = me.getBackground(),
56434 items = me.getItems(),
56435 item, i, ln;
56436
56437 // Cannot render before the surface is placed.
56438 if (!region) {
56439 return;
56440 }
56441
56442 // This will also check the dirty flags of the sprites.
56443 me.orderByZIndex();
56444 if (me.getDirty()) {
56445 me.clear();
56446 me.clearTransform();
56447
56448 if (background) {
56449 me.renderSprite(background);
56450 }
56451
56452 for (i = 0, ln = items.length; i < ln; i++) {
56453 item = items[i];
56454 if (false === me.renderSprite(item)) {
56455 return;
56456 }
56457 item.attr.textPositionCount = me.textPosition;
56458 }
56459
56460 me.setDirty(false);
56461 }
56462 },
56463
56464 /**
56465 * @private
56466 * Renders a single sprite into the surface.
56467 * Do not call it from outside `renderFrame` method.
56468 *
56469 * @param {Ext.draw.sprite.Sprite} sprite The Sprite to be rendered.
56470 * @return {Boolean} returns `false` to stop the rendering to continue.
56471 */
56472 renderSprite: Ext.emptyFn,
56473
56474 /**
56475 * @private
56476 * Clears the current transformation state on the surface.
56477 */
56478 clearTransform: Ext.emptyFn,
56479
56480 /**
56481 * Returns 'true' if the surface is dirty.
56482 * @return {Boolean} 'true' if the surface is dirty
56483 */
56484 getDirty: function () {
56485 return this._dirty;
56486 },
56487
56488 /**
56489 * Destroys the surface. This is done by removing all components from it and
56490 * also removing its reference to a DOM element.
56491 *
56492 * For example:
56493 *
56494 * drawComponent.surface.destroy();
56495 */
56496 destroy: function () {
56497 var me = this;
56498 me.removeAll();
56499 me.setBackground(null);
56500 me.predecessors = null;
56501 me.successors = null;
56502 me.callSuper();
56503 }
56504 });
56505
56506
56507
56508 /**
56509 * @class Ext.draw.engine.SvgContext
56510 *
56511 * A class that imitates a canvas context but generates svg elements instead.
56512 */
56513 Ext.define('Ext.draw.engine.SvgContext', {
56514 /**
56515 * @private
56516 * Properties to be saved/restored in `save` and `restore` method.
56517 */
56518 toSave: ["strokeOpacity", "strokeStyle", "fillOpacity", "fillStyle", "globalAlpha", "lineWidth", "lineCap",
56519 "lineJoin", "lineDash", "lineDashOffset", "miterLimit", "shadowOffsetX", "shadowOffsetY", "shadowBlur",
56520 "shadowColor", "globalCompositeOperation", "position"],
56521
56522 "strokeOpacity": 1,
56523 "strokeStyle": "none",
56524 "fillOpacity": 1,
56525 "fillStyle": "none",
56526 "lineDash": [],
56527 "lineDashOffset": 0,
56528 "globalAlpha": 1,
56529 "lineWidth": 1,
56530 "lineCap": "butt",
56531 "lineJoin": "miter",
56532 "miterLimit": 10,
56533 "shadowOffsetX": 0,
56534 "shadowOffsetY": 0,
56535 "shadowBlur": 0,
56536 "shadowColor": "none",
56537 "globalCompositeOperation": "src",
56538
56539 urlStringRe: /^url\(#([\w\-]+)\)$/,
56540
56541 constructor: function (SvgSurface) {
56542 this.surface = SvgSurface;
56543 this.status = [];
56544 this.matrix = new Ext.draw.Matrix();
56545 this.path = null;
56546 this.clear();
56547 },
56548
56549 /**
56550 * Clears the context.
56551 */
56552 clear: function () {
56553 this.group = this.surface.mainGroup;
56554 this.position = 0;
56555 this.path = null;
56556 },
56557
56558 /**
56559 * @private
56560 * @param {String} tag
56561 * @return {*}
56562 */
56563 getElement: function (tag) {
56564 return this.surface.getSvgElement(this.group, tag, this.position++);
56565 },
56566
56567 /**
56568 * @private
56569 *
56570 * Destroys the DOM element and all associated gradients.
56571 *
56572 * @param element {HTMLElement|Ext.dom.Element|String} DOM element.
56573 */
56574 removeElement: function (element) {
56575 var element = Ext.fly(element),
56576 fill, stroke, fillMatch, strokeMatch,
56577 gradients, gradient, key;
56578
56579 if (!element) {
56580 return;
56581 }
56582 if (element.dom.tagName === 'g') {
56583 gradients = element.dom.gradients;
56584 for (key in gradients) {
56585 gradients[key].destroy();
56586 }
56587 } else {
56588 fill = element.getAttribute('fill');
56589 stroke = element.getAttribute('stroke');
56590 fillMatch = fill && fill.match(this.urlStringRe);
56591 strokeMatch = stroke && stroke.match(this.urlStringRe);
56592 if (fillMatch && fillMatch[1]) {
56593 gradient = Ext.fly(fillMatch[1]);
56594 if (gradient) {
56595 gradient.destroy();
56596 }
56597 }
56598 if (strokeMatch && strokeMatch[1]) {
56599 gradient = Ext.fly(strokeMatch[1]);
56600 if (gradient) {
56601 gradient.destroy();
56602 }
56603 }
56604 }
56605 element.destroy();
56606 },
56607
56608 /**
56609 * Pushes the context state to the state stack.
56610 */
56611 save: function () {
56612 var toSave = this.toSave,
56613 obj = {},
56614 group = this.getElement('g'),
56615 key, i;
56616
56617 for (i = 0; i < toSave.length; i++) {
56618 key = toSave[i];
56619 if (key in this) {
56620 obj[key] = this[key];
56621 }
56622 }
56623 this.position = 0;
56624 obj.matrix = this.matrix.clone();
56625 this.status.push(obj);
56626 this.group = group;
56627 return group;
56628 },
56629
56630 /**
56631 * Pops the state stack and restores the state.
56632 */
56633 restore: function () {
56634 var toSave = this.toSave,
56635 obj = this.status.pop(),
56636 children = this.group.dom.childNodes,
56637 key, i;
56638
56639 // Removing extra DOM elements that were not reused.
56640 while (children.length > this.position) {
56641 this.removeElement(children[children.length - 1]);
56642 }
56643 for (i = 0; i < toSave.length; i++) {
56644 key = toSave[i];
56645 if (key in obj) {
56646 this[key] = obj[key];
56647 } else {
56648 delete this[key];
56649 }
56650 }
56651
56652 this.setTransform.apply(this, obj.matrix.elements);
56653 this.group = this.group.getParent();
56654 },
56655
56656 /**
56657 * Changes the transformation matrix to apply the matrix given by the arguments as described below.
56658 * @param {Number} xx
56659 * @param {Number} yx
56660 * @param {Number} xy
56661 * @param {Number} yy
56662 * @param {Number} dx
56663 * @param {Number} dy
56664 */
56665 transform: function (xx, yx, xy, yy, dx, dy) {
56666 if (this.path) {
56667 var inv = Ext.draw.Matrix.fly([xx, yx, xy, yy, dx, dy]).inverse();
56668 this.path.transform(inv);
56669 }
56670 this.matrix.append(xx, yx, xy, yy, dx, dy);
56671 },
56672
56673 /**
56674 * Changes the transformation matrix to the matrix given by the arguments as described below.
56675 * @param {Number} xx
56676 * @param {Number} yx
56677 * @param {Number} xy
56678 * @param {Number} yy
56679 * @param {Number} dx
56680 * @param {Number} dy
56681 */
56682 setTransform: function (xx, yx, xy, yy, dx, dy) {
56683 if (this.path) {
56684 this.path.transform(this.matrix);
56685 }
56686 this.matrix.reset();
56687 this.transform(xx, yx, xy, yy, dx, dy);
56688 },
56689
56690 /**
56691 * Scales the current context by the specified horizontal (x) and vertical (y) factors.
56692 * @param {Number} x The horizontal scaling factor, where 1 equals unity or 100% scale.
56693 * @param {Number} y The vertical scaling factor.
56694 */
56695 scale: function (x, y) {
56696 this.transform(x, 0, 0, y, 0, 0);
56697 },
56698
56699 /**
56700 * Rotates the current context coordinates (that is, a transformation matrix).
56701 * @param {Number} angle The rotation angle, in radians.
56702 */
56703 rotate: function (angle) {
56704 var xx = Math.cos(angle),
56705 yx = Math.sin(angle),
56706 xy = -Math.sin(angle),
56707 yy = Math.cos(angle);
56708 this.transform(xx, yx, xy, yy, 0, 0);
56709 },
56710
56711 /**
56712 * Specifies values to move the origin point in a canvas.
56713 * @param {Number} x The value to add to horizontal (or x) coordinates.
56714 * @param {Number} y The value to add to vertical (or y) coordinates.
56715 */
56716 translate: function (x, y) {
56717 this.transform(1, 0, 0, 1, x, y);
56718 },
56719
56720 setGradientBBox: function (bbox) {
56721 this.bbox = bbox;
56722 },
56723
56724 /**
56725 * Resets the current default path.
56726 */
56727 beginPath: function () {
56728 this.path = new Ext.draw.Path();
56729 },
56730
56731 /**
56732 * Creates a new subpath with the given point.
56733 * @param {Number} x
56734 * @param {Number} y
56735 */
56736 moveTo: function (x, y) {
56737 if (!this.path) {
56738 this.beginPath();
56739 }
56740 this.path.moveTo(x, y);
56741 this.path.element = null;
56742 },
56743
56744 /**
56745 * Adds the given point to the current subpath, connected to the previous one by a straight line.
56746 * @param {Number} x
56747 * @param {Number} y
56748 */
56749 lineTo: function (x, y) {
56750 if (!this.path) {
56751 this.beginPath();
56752 }
56753 this.path.lineTo(x, y);
56754 this.path.element = null;
56755 },
56756
56757 /**
56758 * Adds a new closed subpath to the path, representing the given rectangle.
56759 * @param {Number} x
56760 * @param {Number} y
56761 * @param {Number} width
56762 * @param {Number} height
56763 */
56764 rect: function (x, y, width, height) {
56765 this.moveTo(x, y);
56766 this.lineTo(x + width, y);
56767 this.lineTo(x + width, y + height);
56768 this.lineTo(x, y + height);
56769 this.closePath();
56770 },
56771
56772 /**
56773 * Paints the box that outlines the given rectangle onto the canvas, using the current stroke style.
56774 * @param {Number} x
56775 * @param {Number} y
56776 * @param {Number} width
56777 * @param {Number} height
56778 */
56779 strokeRect: function (x, y, width, height) {
56780 this.beginPath();
56781 this.rect(x, y, width, height);
56782 this.stroke();
56783 },
56784
56785 /**
56786 * Paints the given rectangle onto the canvas, using the current fill style.
56787 * @param {Number} x
56788 * @param {Number} y
56789 * @param {Number} width
56790 * @param {Number} height
56791 */
56792 fillRect: function (x, y, width, height) {
56793 this.beginPath();
56794 this.rect(x, y, width, height);
56795 this.fill();
56796 },
56797
56798 /**
56799 * Marks the current subpath as closed, and starts a new subpath with a point the same as the start and end of the newly closed subpath.
56800 */
56801 closePath: function () {
56802 if (!this.path) {
56803 this.beginPath();
56804 }
56805 this.path.closePath();
56806 this.path.element = null;
56807 },
56808
56809 /**
56810 * Arc command using svg parameters.
56811 * @param {Number} r1
56812 * @param {Number} r2
56813 * @param {Number} rotation
56814 * @param {Number} large
56815 * @param {Number} swipe
56816 * @param {Number} x2
56817 * @param {Number} y2
56818 */
56819 arcSvg: function (r1, r2, rotation, large, swipe, x2, y2) {
56820 if (!this.path) {
56821 this.beginPath();
56822 }
56823 this.path.arcSvg(r1, r2, rotation, large, swipe, x2, y2);
56824 this.path.element = null;
56825 },
56826
56827 /**
56828 * Adds points to the subpath such that the arc described by the circumference of the circle described by the arguments, starting at the given start angle and ending at the given end angle, going in the given direction (defaulting to clockwise), is added to the path, connected to the previous point by a straight line.
56829 * @param {Number} x
56830 * @param {Number} y
56831 * @param {Number} radius
56832 * @param {Number} startAngle
56833 * @param {Number} endAngle
56834 * @param {Number} anticlockwise
56835 */
56836 arc: function (x, y, radius, startAngle, endAngle, anticlockwise) {
56837 if (!this.path) {
56838 this.beginPath();
56839 }
56840 this.path.arc(x, y, radius, startAngle, endAngle, anticlockwise);
56841 this.path.element = null;
56842 },
56843
56844 /**
56845 * Adds points to the subpath such that the arc described by the circumference of the ellipse described by the arguments, starting at the given start angle and ending at the given end angle, going in the given direction (defaulting to clockwise), is added to the path, connected to the previous point by a straight line.
56846 * @param {Number} x
56847 * @param {Number} y
56848 * @param {Number} radiusX
56849 * @param {Number} radiusY
56850 * @param {Number} rotation
56851 * @param {Number} startAngle
56852 * @param {Number} endAngle
56853 * @param {Number} anticlockwise
56854 */
56855 ellipse: function (x, y, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise) {
56856 if (!this.path) {
56857 this.beginPath();
56858 }
56859 this.path.ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise);
56860 this.path.element = null;
56861 },
56862
56863 /**
56864 * Adds an arc with the given control points and radius to the current subpath, connected to the previous point by a straight line.
56865 * If two radii are provided, the first controls the width of the arc's ellipse, and the second controls the height. If only one is provided, or if they are the same, the arc is from a circle.
56866 * In the case of an ellipse, the rotation argument controls the clockwise inclination of the ellipse relative to the x-axis.
56867 * @param {Number} x1
56868 * @param {Number} y1
56869 * @param {Number} x2
56870 * @param {Number} y2
56871 * @param {Number} radiusX
56872 * @param {Number} radiusY
56873 * @param {Number} rotation
56874 */
56875 arcTo: function (x1, y1, x2, y2, radiusX, radiusY, rotation) {
56876 if (!this.path) {
56877 this.beginPath();
56878 }
56879 this.path.arcTo(x1, y1, x2, y2, radiusX, radiusY, rotation);
56880 this.path.element = null;
56881 },
56882
56883 /**
56884 * Adds the given point to the current subpath, connected to the previous one by a cubic Bézier curve with the given control points.
56885 * @param {Number} x1
56886 * @param {Number} y1
56887 * @param {Number} x2
56888 * @param {Number} y2
56889 * @param {Number} x3
56890 * @param {Number} y3
56891 */
56892 bezierCurveTo: function (x1, y1, x2, y2, x3, y3) {
56893 if (!this.path) {
56894 this.beginPath();
56895 }
56896 this.path.bezierCurveTo(x1, y1, x2, y2, x3, y3);
56897 this.path.element = null;
56898 },
56899
56900 /**
56901 * Strokes the given text at the given position. If a maximum width is provided, the text will be scaled to fit that width if necessary.
56902 * @param {String} text
56903 * @param {Number} x
56904 * @param {Number} y
56905 */
56906 strokeText: function (text, x, y) {
56907 text = String(text);
56908 if (this.strokeStyle) {
56909 var element = this.getElement('text'),
56910 tspan = this.surface.getSvgElement(element, 'tspan', 0);
56911 this.surface.setElementAttributes(element, {
56912 "x": x,
56913 "y": y,
56914 "transform": this.matrix.toSvg(),
56915 "stroke": this.strokeStyle,
56916 "fill": "none",
56917 "opacity": this.globalAlpha,
56918 "stroke-opacity": this.strokeOpacity,
56919 "style": "font: " + this.font
56920 });
56921 if (this.lineDash.length) {
56922 this.surface.setElementAttributes(element, {
56923 "stroke-dasharray": this.lineDash.join(','),
56924 "stroke-dashoffset": this.lineDashOffset
56925 });
56926 }
56927 if (tspan.dom.firstChild) {
56928 tspan.dom.removeChild(tspan.dom.firstChild);
56929 }
56930 this.surface.setElementAttributes(tspan, {
56931 "alignment-baseline": "middle",
56932 "baseline-shift": "-50%"
56933 });
56934 tspan.appendChild(document.createTextNode(Ext.String.htmlDecode(text)));
56935 }
56936 },
56937
56938 /**
56939 * Fills the given text at the given position. If a maximum width is provided, the text will be scaled to fit that width if necessary.
56940 * @param {String} text
56941 * @param {Number} x
56942 * @param {Number} y
56943 */
56944 fillText: function (text, x, y) {
56945 text = String(text);
56946 if (this.fillStyle) {
56947 var element = this.getElement('text'),
56948 tspan = this.surface.getSvgElement(element, 'tspan', 0);
56949 this.surface.setElementAttributes(element, {
56950 "x": x,
56951 "y": y,
56952 "transform": this.matrix.toSvg(),
56953 "fill": this.fillStyle,
56954 "opacity": this.globalAlpha,
56955 "fill-opacity": this.fillOpacity,
56956 "style": "font: " + this.font
56957 });
56958 if (tspan.dom.firstChild) {
56959 tspan.dom.removeChild(tspan.dom.firstChild);
56960 }
56961 this.surface.setElementAttributes(tspan, {
56962 "alignment-baseline": "middle",
56963 "baseline-shift": "-50%"
56964 });
56965 tspan.appendChild(document.createTextNode(Ext.String.htmlDecode(text)));
56966 }
56967 },
56968
56969 /**
56970 * Draws the given image onto the canvas.
56971 * If the first argument isn't an img, canvas, or video element, throws a TypeMismatchError exception. If the image has no image data, throws an InvalidStateError exception. If the one of the source rectangle dimensions is zero, throws an IndexSizeError exception. If the image isn't yet fully decoded, then nothing is drawn.
56972 * @param {HTMLElement} image
56973 * @param {Number} sx
56974 * @param {Number} sy
56975 * @param {Number} sw
56976 * @param {Number} sh
56977 * @param {Number} dx
56978 * @param {Number} dy
56979 * @param {Number} dw
56980 * @param {Number} dh
56981 */
56982 drawImage: function (image, sx, sy, sw, sh, dx, dy, dw, dh) {
56983 var me = this,
56984 element = me.getElement('image'),
56985 x = sx, y = sy,
56986 width = typeof sw === 'undefined' ? image.width : sw,
56987 height = typeof sh === 'undefined' ? image.height : sh,
56988 viewBox = null;
56989 if (typeof dh !== 'undefined') {
56990 viewBox = sx + " " + sy + " " + sw + " " + sh;
56991 x = dx;
56992 y = dy;
56993 width = dw;
56994 height = dh;
56995 }
56996 element.dom.setAttributeNS("http:/" + "/www.w3.org/1999/xlink", "href", image.src);
56997 me.surface.setElementAttributes(element, {
56998 viewBox: viewBox,
56999 x: x,
57000 y: y,
57001 width: width,
57002 height: height,
57003 opacity: me.globalAlpha,
57004 transform: me.matrix.toSvg()
57005 });
57006 },
57007
57008 /**
57009 * Fills the subpaths of the current default path or the given path with the current fill style.
57010 */
57011 fill: function () {
57012 if (!this.path) {
57013 return;
57014 }
57015 if (this.fillStyle) {
57016 var path,
57017 fillGradient = this.fillGradient,
57018 bbox = this.bbox,
57019 element = this.path.element;
57020 if (!element) {
57021 path = this.path.toString();
57022 element = this.path.element = this.getElement('path');
57023 this.surface.setElementAttributes(element, {
57024 "d": path,
57025 "transform": this.matrix.toSvg()
57026 });
57027 }
57028 this.surface.setElementAttributes(element, {
57029 "fill": fillGradient && bbox ? fillGradient.generateGradient(this, bbox) : this.fillStyle,
57030 "fill-opacity": this.fillOpacity * this.globalAlpha
57031 });
57032 }
57033 },
57034
57035 /**
57036 * Strokes the subpaths of the current default path or the given path with the current stroke style.
57037 */
57038 stroke: function () {
57039 if (!this.path) {
57040 return;
57041 }
57042 if (this.strokeStyle) {
57043 var path,
57044 strokeGradient = this.strokeGradient,
57045 bbox = this.bbox,
57046 element = this.path.element;
57047 if (!element || !this.path.svgString) {
57048 path = this.path.toString();
57049 element = this.path.element = this.getElement('path');
57050 this.surface.setElementAttributes(element, {
57051 "fill": "none",
57052 "d": path,
57053 "transform": this.matrix.toSvg()
57054 });
57055 }
57056 this.surface.setElementAttributes(element, {
57057 "stroke": strokeGradient && bbox ? strokeGradient.generateGradient(this, bbox) : this.strokeStyle,
57058 "stroke-linecap": this.lineCap,
57059 "stroke-linejoin": this.lineJoin,
57060 "stroke-width": this.lineWidth,
57061 "stroke-opacity": this.strokeOpacity * this.globalAlpha
57062 });
57063 if (this.lineDash.length) {
57064 this.surface.setElementAttributes(element, {
57065 "stroke-dasharray": this.lineDash.join(','),
57066 "stroke-dashoffset": this.lineDashOffset
57067 });
57068 }
57069 }
57070 },
57071
57072 /**
57073 * @protected
57074 *
57075 * Note: After the method guarantees the transform matrix will be inverted.
57076 * @param {Object} attr The attribute object
57077 * @param {Boolean} [transformFillStroke] Indicate whether to transform fill and stroke. If this is not
57078 * given, then uses `attr.transformFillStroke` instead.
57079 */
57080 fillStroke: function (attr, transformFillStroke) {
57081 var ctx = this,
57082 fillStyle = ctx.fillStyle,
57083 strokeStyle = ctx.strokeStyle,
57084 fillOpacity = ctx.fillOpacity,
57085 strokeOpacity = ctx.strokeOpacity;
57086
57087 if (transformFillStroke === undefined) {
57088 transformFillStroke = attr.transformFillStroke;
57089 }
57090
57091 if (!transformFillStroke) {
57092 attr.inverseMatrix.toContext(ctx);
57093 }
57094
57095 if (fillStyle && fillOpacity !== 0) {
57096 ctx.fill();
57097 }
57098
57099 if (strokeStyle && strokeOpacity !== 0) {
57100 ctx.stroke();
57101 }
57102 },
57103
57104 appendPath: function (path) {
57105 this.path = path.clone();
57106 },
57107
57108 /**
57109 * Returns an object that represents a linear gradient that paints along the line given by the coordinates represented by the arguments.
57110 * @param {Number} x0
57111 * @param {Number} y0
57112 * @param {Number} x1
57113 * @param {Number} y1
57114 * @return {Ext.draw.engine.SvgContext.Gradient}
57115 */
57116 createLinearGradient: function (x0, y0, x1, y1) {
57117 var me = this,
57118 element = me.surface.getNextDef('linearGradient'),
57119 gradients = me.group.dom.gradients || (me.group.dom.gradients = {}),
57120 gradient;
57121 me.surface.setElementAttributes(element, {
57122 "x1": x0,
57123 "y1": y0,
57124 "x2": x1,
57125 "y2": y1,
57126 "gradientUnits": "userSpaceOnUse"
57127 });
57128 gradient = new Ext.draw.engine.SvgContext.Gradient(me, me.surface, element);
57129 gradients[element.dom.id] = gradient;
57130 return gradient;
57131 },
57132
57133 /**
57134 * Returns a CanvasGradient object that represents a radial gradient that paints along the cone given by the circles represented by the arguments.
57135 * If either of the radii are negative, throws an IndexSizeError exception.
57136 * @param {Number} x0
57137 * @param {Number} y0
57138 * @param {Number} r0
57139 * @param {Number} x1
57140 * @param {Number} y1
57141 * @param {Number} r1
57142 * @return {Ext.draw.engine.SvgContext.Gradient}
57143 */
57144 createRadialGradient: function (x0, y0, r0, x1, y1, r1) {
57145 var me = this,
57146 element = me.surface.getNextDef('radialGradient'),
57147 gradients = me.group.dom.gradients || (me.group.dom.gradients = {}),
57148 gradient;
57149 me.surface.setElementAttributes(element, {
57150 "fx": x0,
57151 "fy": y0,
57152 "cx": x1,
57153 "cy": y1,
57154 "r": r1,
57155 "gradientUnits": "userSpaceOnUse"
57156 });
57157 gradient = new Ext.draw.engine.SvgContext.Gradient(me, me.surface, element, r0 / r1);
57158 gradients[element.dom.id] = gradient;
57159 return gradient;
57160 }
57161 });
57162
57163 /**
57164 * @class Ext.draw.engine.SvgContext.Gradient
57165 */
57166 Ext.define("Ext.draw.engine.SvgContext.Gradient", {
57167
57168 statics: {
57169 map: {}
57170 },
57171
57172 constructor: function (ctx, surface, element, compression) {
57173 var map = this.statics().map,
57174 oldInstance;
57175
57176 // Because of the way Ext.draw.engine.Svg.getNextDef works,
57177 // there is no guarantee that an existing DOM element from the 'defs' section won't be used
57178 // for the 'element' param.
57179 oldInstance = map[element.dom.id];
57180 if (oldInstance) {
57181 oldInstance.element = null;
57182 }
57183 map[element.dom.id] = this;
57184
57185 this.ctx = ctx;
57186 this.surface = surface;
57187 this.element = element;
57188 this.position = 0;
57189 this.compression = compression || 0;
57190 },
57191
57192 /**
57193 * Adds a color stop with the given color to the gradient at the given offset. 0.0 is the offset at one end of the gradient, 1.0 is the offset at the other end.
57194 * @param {Number} offset
57195 * @param {String} color
57196 */
57197 addColorStop: function (offset, color) {
57198 var stop = this.surface.getSvgElement(this.element, 'stop', this.position++),
57199 compression = this.compression;
57200 this.surface.setElementAttributes(stop, {
57201 "offset": (((1 - compression) * offset + compression) * 100).toFixed(2) + '%',
57202 "stop-color": color,
57203 "stop-opacity": Ext.draw.Color.fly(color).a.toFixed(15)
57204 });
57205 },
57206
57207 toString: function () {
57208 var children = this.element.dom.childNodes;
57209 // Removing surplus stops in case existing gradient element with more stops was reused.
57210 while (children.length > this.position) {
57211 Ext.fly(children[children.length - 1]).destroy();
57212 }
57213 return 'url(#' + this.element.getId() + ')';
57214 },
57215
57216 destroy: function () {
57217 var map = this.statics().map,
57218 element = this.element;
57219 if (element) {
57220 delete map[element.dom.id];
57221 element.destroy();
57222 }
57223 this.callSuper();
57224 }
57225 });
57226
57227 /**
57228 * @class Ext.draw.engine.Svg
57229 * @extends Ext.draw.Surface
57230 *
57231 * SVG engine.
57232 */
57233 Ext.define('Ext.draw.engine.Svg', {
57234 extend: Ext.draw.Surface ,
57235
57236
57237 statics: {
57238 BBoxTextCache: {}
57239 },
57240
57241 config: {
57242 /**
57243 * Nothing needs to be done in high precision mode.
57244 */
57245 highPrecision: false
57246 },
57247
57248 getElementConfig: function () {
57249 return {
57250 reference: 'element',
57251 style: {
57252 position: 'absolute'
57253 },
57254 children: [
57255 {
57256 reference: 'innerElement',
57257 style: {
57258 width: '100%',
57259 height: '100%',
57260 position: 'relative'
57261 },
57262 children: [
57263 {
57264 tag: 'svg',
57265 reference: 'svgElement',
57266 namespace: "http://www.w3.org/2000/svg",
57267 version: 1.1,
57268 cls: 'x-surface'
57269 }
57270 ]
57271 }
57272 ]
57273 };
57274 },
57275
57276 constructor: function (config) {
57277 var me = this;
57278 me.callSuper([config]);
57279 me.mainGroup = me.createSvgNode("g");
57280 me.defElement = me.createSvgNode("defs");
57281 // me.svgElement is assigned in element creation of Ext.Component.
57282 me.svgElement.appendChild(me.mainGroup);
57283 me.svgElement.appendChild(me.defElement);
57284 me.ctx = new Ext.draw.engine.SvgContext(me);
57285 },
57286
57287 /**
57288 * Creates a DOM element under the SVG namespace of the given type.
57289 * @param {String} type The type of the SVG DOM element.
57290 * @return {*} The created element.
57291 */
57292 createSvgNode: function (type) {
57293 var node = document.createElementNS("http://www.w3.org/2000/svg", type);
57294 return Ext.get(node);
57295 },
57296
57297 /**
57298 * @private
57299 * Returns the SVG DOM element at the given position. If it does not already exist or is a different element tag
57300 * it will be created and inserted into the DOM.
57301 * @param {Ext.dom.Element} group The parent DOM element.
57302 * @param {String} tag The SVG element tag.
57303 * @param {Number} position The position of the element in the DOM.
57304 * @return {Ext.dom.Element} The SVG element.
57305 */
57306 getSvgElement: function (group, tag, position) {
57307 var element;
57308 if (group.dom.childNodes.length > position) {
57309 element = group.dom.childNodes[position];
57310 if (element.tagName === tag) {
57311 return Ext.get(element);
57312 } else {
57313 Ext.destroy(element);
57314 }
57315 }
57316
57317 element = Ext.get(this.createSvgNode(tag));
57318 if (position === 0) {
57319 group.insertFirst(element);
57320 } else {
57321 element.insertAfter(Ext.fly(group.dom.childNodes[position - 1]));
57322 }
57323 element.cache = {};
57324 return element;
57325 },
57326
57327 /**
57328 * @private
57329 * Applies attributes to the given element.
57330 * @param {Ext.dom.Element} element The DOM element to be applied.
57331 * @param {Object} attributes The attributes to apply to the element.
57332 */
57333 setElementAttributes: function (element, attributes) {
57334 var dom = element.dom,
57335 cache = element.cache,
57336 name, value;
57337 for (name in attributes) {
57338 value = attributes[name];
57339 if (cache[name] !== value) {
57340 cache[name] = value;
57341 dom.setAttribute(name, value);
57342 }
57343 }
57344 },
57345
57346 /**
57347 * @private
57348 * Gets the next reference element under the SVG 'defs' tag.
57349 * @param {String} tagName The type of reference element.
57350 * @return {Ext.dom.Element} The reference element.
57351 */
57352 getNextDef: function (tagName) {
57353 return this.getSvgElement(this.defElement, tagName, this.defPosition++);
57354 },
57355
57356 /**
57357 * @inheritdoc
57358 */
57359 clearTransform: function () {
57360 var me = this;
57361 me.mainGroup.set({transform: me.matrix.toSvg()});
57362 },
57363
57364 /**
57365 * @inheritdoc
57366 */
57367 clear: function () {
57368 this.ctx.clear();
57369 this.defPosition = 0;
57370 },
57371
57372 /**
57373 * @inheritdoc
57374 */
57375 renderSprite: function (sprite) {
57376 var me = this,
57377 region = me.getRegion(),
57378 ctx = me.ctx;
57379 if (sprite.attr.hidden || sprite.attr.opacity === 0) {
57380 ctx.save();
57381 ctx.restore();
57382 return;
57383 }
57384 try {
57385 sprite.element = ctx.save();
57386 sprite.preRender(this);
57387 sprite.useAttributes(ctx, region);
57388 if (false === sprite.render(this, ctx, [0, 0, region[2], region[3]])) {
57389 return false;
57390 }
57391 sprite.setDirty(false);
57392 } finally {
57393 ctx.restore();
57394 }
57395 },
57396
57397 /**
57398 * Destroys the Canvas element and prepares it for Garbage Collection.
57399 */
57400 destroy: function (path, matrix, band) {
57401 var me = this;
57402 me.ctx.destroy();
57403 me.mainGroup.destroy();
57404 delete me.mainGroup;
57405 delete me.ctx;
57406 me.callSuper(arguments);
57407 },
57408
57409 remove: function (sprite, destroySprite) {
57410 if (sprite && sprite.element) {
57411 //if sprite has an associated svg element remove it from the surface
57412 if (this.ctx) {
57413 this.ctx.removeElement(sprite.element);
57414 } else {
57415 sprite.element.destroy();
57416 }
57417 sprite.element = null;
57418 }
57419 this.callSuper(arguments);
57420 }
57421 });
57422
57423 /**
57424 * Provides specific methods to draw with 2D Canvas element.
57425 */
57426 Ext.define('Ext.draw.engine.Canvas', {
57427 extend: Ext.draw.Surface ,
57428 config: {
57429 /**
57430 * @cfg {Boolean} highPrecision
57431 * True to have the canvas use JavaScript Number instead of single precision floating point for transforms.
57432 *
57433 * For example, when using huge data to plot line series, the transform matrix of the canvas will have
57434 * a big element. Due to the implementation of SVGMatrix, the elements are restored by 32-bits floats, which
57435 * will work incorrectly. To compensate that, we enable the canvas context to perform all the transform by
57436 * JavaScript. Do not use it if you are not encountering 32-bits floating point errors problem since it will
57437 * have a performance penalty.
57438 */
57439 highPrecision: false
57440 },
57441
57442
57443 statics: {
57444 contextOverrides: {
57445 /**
57446 * @ignore
57447 */
57448 setGradientBBox: function (bbox) {
57449 this.bbox = bbox;
57450 },
57451
57452 /**
57453 * Fills the subpaths of the current default path or the given path with the current fill style.
57454 * @ignore
57455 */
57456 fill: function () {
57457 var fillStyle = this.fillStyle,
57458 fillGradient = this.fillGradient,
57459 fillOpacity = this.fillOpacity,
57460 rgba = 'rgba(0, 0, 0, 0)',
57461 rgba0 = 'rgba(0, 0, 0, 0.0)',
57462 bbox = this.bbox,
57463 alpha = this.globalAlpha;
57464
57465 if (fillStyle !== rgba && fillStyle !== rgba0 && fillOpacity !== 0) {
57466 if (fillGradient && bbox) {
57467 this.fillStyle = fillGradient.generateGradient(this, bbox);
57468 }
57469
57470 if (fillOpacity !== 1) {
57471 this.globalAlpha = alpha * fillOpacity;
57472 }
57473 this.$fill();
57474 if (fillOpacity !== 1) {
57475 this.globalAlpha = alpha;
57476 }
57477
57478 if (fillGradient && bbox) {
57479 this.fillStyle = fillStyle;
57480 }
57481 }
57482 },
57483
57484 /**
57485 * Strokes the subpaths of the current default path or the given path with the current stroke style.
57486 * @ignore
57487 */
57488 stroke: function () {
57489 var strokeStyle = this.strokeStyle,
57490 strokeGradient = this.strokeGradient,
57491 strokeOpacity = this.strokeOpacity,
57492 rgba = 'rgba(0, 0, 0, 0)',
57493 rgba0 = 'rgba(0, 0, 0, 0.0)',
57494 bbox = this.bbox,
57495 alpha = this.globalAlpha;
57496
57497 if (strokeStyle !== rgba && strokeStyle !== rgba0 && strokeOpacity !== 0) {
57498 if (strokeGradient && bbox) {
57499 this.strokeStyle = strokeGradient.generateGradient(this, bbox);
57500 }
57501
57502 if (strokeOpacity !== 1) {
57503 this.globalAlpha = alpha * strokeOpacity;
57504 }
57505 this.$stroke();
57506 if (strokeOpacity !== 1) {
57507 this.globalAlpha = alpha;
57508 }
57509
57510 if (strokeGradient && bbox) {
57511 this.strokeStyle = strokeStyle;
57512 }
57513 }
57514 },
57515
57516 /**
57517 * @ignore
57518 */
57519 fillStroke: function (attr, transformFillStroke) {
57520 var ctx = this,
57521 fillStyle = this.fillStyle,
57522 fillOpacity = this.fillOpacity,
57523 strokeStyle = this.strokeStyle,
57524 strokeOpacity = this.strokeOpacity,
57525 shadowColor = ctx.shadowColor,
57526 shadowBlur = ctx.shadowBlur,
57527 rgba = 'rgba(0, 0, 0, 0)',
57528 rgba0 = 'rgba(0, 0, 0, 0.0)';
57529
57530 if (transformFillStroke === undefined) {
57531 transformFillStroke = attr.transformFillStroke;
57532 }
57533
57534 if (!transformFillStroke) {
57535 attr.inverseMatrix.toContext(ctx);
57536 }
57537 if (fillStyle !== rgba && fillStyle !== rgba0 && fillOpacity !== 0) {
57538 ctx.fill();
57539 ctx.shadowColor = 'rgba(0,0,0,0)';
57540 ctx.shadowBlur = 0;
57541 }
57542 if (strokeStyle !== rgba && strokeStyle !== rgba0 && strokeOpacity !== 0) {
57543 ctx.stroke();
57544 }
57545 ctx.shadowColor = shadowColor;
57546 ctx.shadowBlur = shadowBlur;
57547 },
57548
57549 /**
57550 * Adds points to the subpath such that the arc described by the circumference of the
57551 * ellipse described by the arguments, starting at the given start angle and ending at
57552 * the given end angle, going in the given direction (defaulting to clockwise), is added
57553 * to the path, connected to the previous point by a straight line.
57554 * @ignore
57555 */
57556 ellipse: function (cx, cy, rx, ry, rotation, start, end, anticlockwise) {
57557 var cos = Math.cos(rotation),
57558 sin = Math.sin(rotation);
57559 this.transform(cos * rx, sin * rx, -sin * ry, cos * ry, cx, cy);
57560 this.arc(0, 0, 1, start, end, anticlockwise);
57561 this.transform(
57562 cos / rx, -sin / ry,
57563 sin / rx, cos / ry,
57564 -(cos * cx + sin * cy) / rx, (sin * cx - cos * cy) / ry);
57565 },
57566
57567 /**
57568 * Uses the given path commands to begin a new path on the canvas.
57569 * @ignore
57570 */
57571 appendPath: function (path) {
57572 var me = this,
57573 i = 0, j = 0,
57574 types = path.types,
57575 coords = path.coords,
57576 ln = path.types.length;
57577 me.beginPath();
57578 for (; i < ln; i++) {
57579 switch (types[i]) {
57580 case "M":
57581 me.moveTo(coords[j], coords[j + 1]);
57582 j += 2;
57583 break;
57584 case "L":
57585 me.lineTo(coords[j], coords[j + 1]);
57586 j += 2;
57587 break;
57588 case "C":
57589 me.bezierCurveTo(
57590 coords[j], coords[j + 1],
57591 coords[j + 2], coords[j + 3],
57592 coords[j + 4], coords[j + 5]
57593 );
57594 j += 6;
57595 break;
57596 case "Z":
57597 me.closePath();
57598 break;
57599 }
57600 }
57601 }
57602 }
57603 },
57604
57605 splitThreshold: 1800,
57606
57607 getElementConfig: function () {
57608 return {
57609 reference: 'element',
57610 style: {
57611 position: 'absolute'
57612 },
57613 children: [
57614 {
57615 reference: 'innerElement',
57616 style: {
57617 width: '100%',
57618 height: '100%',
57619 position: 'relative'
57620 }
57621 }
57622 ]
57623 };
57624 },
57625
57626 /**
57627 * @private
57628 *
57629 * Creates the canvas element.
57630 */
57631 createCanvas: function () {
57632 var canvas = Ext.Element.create({
57633 tag: 'canvas',
57634 cls: 'x-surface'
57635 }),
57636 overrides = Ext.draw.engine.Canvas.contextOverrides,
57637 ctx = canvas.dom.getContext('2d'),
57638 backingStoreRatio = ctx.webkitBackingStorePixelRatio ||
57639 ctx.mozBackingStorePixelRatio ||
57640 ctx.msBackingStorePixelRatio ||
57641 ctx.oBackingStorePixelRatio ||
57642 ctx.backingStorePixelRatio || 1,
57643 name;
57644
57645 // Windows Phone does not currently support backingStoreRatio
57646 this.devicePixelRatio /= (Ext.os.is.WindowsPhone) ? window.innerWidth / window.screen.width : backingStoreRatio;
57647
57648 if (ctx.ellipse) {
57649 delete overrides.ellipse;
57650 }
57651
57652 for (name in overrides) {
57653 ctx['$' + name] = ctx[name];
57654 }
57655 Ext.apply(ctx, overrides);
57656
57657 if (this.getHighPrecision()) {
57658 this.enablePrecisionCompensation(ctx);
57659 } else {
57660 this.disablePrecisionCompensation(ctx);
57661 }
57662
57663 this.innerElement.appendChild(canvas);
57664 this.canvases.push(canvas);
57665 this.contexts.push(ctx);
57666 },
57667
57668 /**
57669 * Initialize the canvas element.
57670 */
57671 initElement: function () {
57672 this.callSuper();
57673 this.canvases = [];
57674 this.contexts = [];
57675 this.createCanvas();
57676 this.activeCanvases = 0;
57677 },
57678
57679 updateHighPrecision: function (pc) {
57680 var contexts = this.contexts,
57681 ln = contexts.length,
57682 i, context;
57683
57684 for (i = 0; i < ln; i++) {
57685 context = contexts[i];
57686 if (pc) {
57687 this.enablePrecisionCompensation(context);
57688 } else {
57689 this.disablePrecisionCompensation(context);
57690 }
57691 }
57692 },
57693
57694 precisionMethods: {
57695 rect: false,
57696 fillRect: false,
57697 strokeRect: false,
57698 clearRect: false,
57699 moveTo: false,
57700 lineTo: false,
57701 arc: false,
57702 arcTo: false,
57703 save: false,
57704 restore: false,
57705 updatePrecisionCompensate: false,
57706 setTransform: false,
57707 transform: false,
57708 scale: false,
57709 translate: false,
57710 rotate: false,
57711 quadraticCurveTo: false,
57712 bezierCurveTo: false,
57713 createLinearGradient: false,
57714 createRadialGradient: false,
57715 fillText: false,
57716 strokeText: false,
57717 drawImage: false
57718 },
57719
57720 /**
57721 * @private
57722 * Clears canvas of compensation for canvas' use of single precision floating point.
57723 * @param {CanvasRenderingContext2D} ctx The canvas context.
57724 */
57725 disablePrecisionCompensation: function (ctx) {
57726 var precisionMethods = this.precisionMethods,
57727 name;
57728
57729 for (name in precisionMethods) {
57730 delete ctx[name];
57731 }
57732
57733 this.setDirty(true);
57734 },
57735
57736 /**
57737 * @private
57738 * Compensate for canvas' use of single precision floating point.
57739 * @param {CanvasRenderingContext2D} ctx The canvas context.
57740 */
57741 enablePrecisionCompensation: function (ctx) {
57742 var surface = this,
57743 xx = 1, yy = 1,
57744 dx = 0, dy = 0,
57745 matrix = new Ext.draw.Matrix(),
57746 transStack = [],
57747 comp = {},
57748 originalCtx = ctx.constructor.prototype;
57749
57750 /**
57751 * @class CanvasRenderingContext2D
57752 * @ignore
57753 */
57754 var override = {
57755 /**
57756 * Adds a new closed subpath to the path, representing the given rectangle.
57757 * @return {*}
57758 * @ignore
57759 */
57760 rect: function (x, y, w, h) {
57761 return originalCtx.rect.call(this, x * xx + dx, y * yy + dy, w * xx, h * yy);
57762 },
57763
57764 /**
57765 * Paints the given rectangle onto the canvas, using the current fill style.
57766 * @ignore
57767 */
57768 fillRect: function (x, y, w, h) {
57769 this.updatePrecisionCompensateRect();
57770 originalCtx.fillRect.call(this, x * xx + dx, y * yy + dy, w * xx, h * yy);
57771 this.updatePrecisionCompensate();
57772 },
57773
57774 /**
57775 * Paints the box that outlines the given rectangle onto the canvas, using the current stroke style.
57776 * @ignore
57777 */
57778 strokeRect: function (x, y, w, h) {
57779 this.updatePrecisionCompensateRect();
57780 originalCtx.strokeRect.call(this, x * xx + dx, y * yy + dy, w * xx, h * yy);
57781 this.updatePrecisionCompensate();
57782 },
57783
57784 /**
57785 * Clears all pixels on the canvas in the given rectangle to transparent black.
57786 * @ignore
57787 */
57788 clearRect: function (x, y, w, h) {
57789 return originalCtx.clearRect.call(this, x * xx + dx, y * yy + dy, w * xx, h * yy);
57790 },
57791
57792 /**
57793 * Creates a new subpath with the given point.
57794 * @ignore
57795 */
57796 moveTo: function (x, y) {
57797 return originalCtx.moveTo.call(this, x * xx + dx, y * yy + dy);
57798 },
57799
57800 /**
57801 * Adds the given point to the current subpath, connected to the previous one by a straight line.
57802 * @ignore
57803 */
57804 lineTo: function (x, y) {
57805 return originalCtx.lineTo.call(this, x * xx + dx, y * yy + dy);
57806 },
57807
57808 /**
57809 * Adds points to the subpath such that the arc described by the circumference of the
57810 * circle described by the arguments, starting at the given start angle and ending at
57811 * the given end angle, going in the given direction (defaulting to clockwise), is added
57812 * to the path, connected to the previous point by a straight line.
57813 * @ignore
57814 */
57815 arc: function (x, y, radius, startAngle, endAngle, anticlockwise) {
57816 this.updatePrecisionCompensateRect();
57817 originalCtx.arc.call(this, x * xx + dx, y * xx + dy, radius * xx, startAngle, endAngle, anticlockwise);
57818 this.updatePrecisionCompensate();
57819 },
57820
57821 /**
57822 * Adds an arc with the given control points and radius to the current subpath,
57823 * connected to the previous point by a straight line. If two radii are provided, the
57824 * first controls the width of the arc's ellipse, and the second controls the height. If
57825 * only one is provided, or if they are the same, the arc is from a circle.
57826 *
57827 * In the case of an ellipse, the rotation argument controls the clockwise inclination
57828 * of the ellipse relative to the x-axis.
57829 * @ignore
57830 */
57831 arcTo: function (x1, y1, x2, y2, radius) {
57832 this.updatePrecisionCompensateRect();
57833 originalCtx.arcTo.call(this, x1 * xx + dx, y1 * yy + dy, x2 * xx + dx, y2 * yy + dy, radius * xx);
57834 this.updatePrecisionCompensate();
57835 },
57836
57837 /**
57838 * Pushes the context state to the state stack.
57839 * @ignore
57840 */
57841 save: function () {
57842 transStack.push(matrix);
57843 matrix = matrix.clone();
57844 return originalCtx.save.call(this);
57845 },
57846
57847 /**
57848 * Pops the state stack and restores the state.
57849 * @ignore
57850 */
57851 restore: function () {
57852 matrix = transStack.pop();
57853 originalCtx.restore.call(this);
57854 this.updatePrecisionCompensate();
57855 },
57856
57857 /**
57858 * @ignore
57859 */
57860 updatePrecisionCompensate: function () {
57861 matrix.precisionCompensate(surface.devicePixelRatio, comp);
57862 xx = comp.xx;
57863 yy = comp.yy;
57864 dx = comp.dx;
57865 dy = comp.dy;
57866 return originalCtx.setTransform.call(this, surface.devicePixelRatio, comp.b, comp.c, comp.d, 0, 0);
57867 },
57868
57869 /**
57870 * @ignore
57871 */
57872 updatePrecisionCompensateRect: function () {
57873 matrix.precisionCompensateRect(surface.devicePixelRatio, comp);
57874 xx = comp.xx;
57875 yy = comp.yy;
57876 dx = comp.dx;
57877 dy = comp.dy;
57878 return originalCtx.setTransform.call(this, surface.devicePixelRatio, comp.b, comp.c, comp.d, 0, 0);
57879 },
57880
57881 /**
57882 * Changes the transformation matrix to the matrix given by the arguments as described below.
57883 * @ignore
57884 */
57885 setTransform: function (x2x, x2y, y2x, y2y, newDx, newDy) {
57886 matrix.set(x2x, x2y, y2x, y2y, newDx, newDy);
57887 this.updatePrecisionCompensate();
57888 },
57889
57890 /**
57891 * Changes the transformation matrix to apply the matrix given by the arguments as described below.
57892 * @ignore
57893 */
57894 transform: function (x2x, x2y, y2x, y2y, newDx, newDy) {
57895 matrix.append(x2x, x2y, y2x, y2y, newDx, newDy);
57896 this.updatePrecisionCompensate();
57897 },
57898
57899 /**
57900 * Scales the transformation matrix.
57901 * @return {*}
57902 * @ignore
57903 */
57904 scale: function (sx, sy) {
57905 return this.transform(sx, 0, 0, sy, 0, 0);
57906 },
57907
57908 /**
57909 * Translates the transformation matrix.
57910 * @return {*}
57911 * @ignore
57912 */
57913 translate: function (dx, dy) {
57914 return this.transform(1, 0, 0, 1, dx, dy);
57915 },
57916
57917 /**
57918 * Rotates the transformation matrix.
57919 * @return {*}
57920 * @ignore
57921 */
57922 rotate: function (radians) {
57923 var cos = Math.cos(radians),
57924 sin = Math.sin(radians);
57925 return this.transform(cos, sin, -sin, cos, 0, 0);
57926 },
57927
57928 /**
57929 * Adds the given point to the current subpath, connected to the previous one by a
57930 * quadratic Bézier curve with the given control point.
57931 * @return {*}
57932 * @ignore
57933 */
57934 quadraticCurveTo: function (cx, cy, x, y) {
57935 return originalCtx.quadraticCurveTo.call(this,
57936 cx * xx + dx,
57937 cy * yy + dy,
57938 x * xx + dx,
57939 y * yy + dy
57940 );
57941 },
57942
57943 /**
57944 * Adds the given point to the current subpath, connected to the previous one by a cubic
57945 * Bézier curve with the given control points.
57946 * @return {*}
57947 * @ignore
57948 */
57949 bezierCurveTo: function (c1x, c1y, c2x, c2y, x, y) {
57950 return originalCtx.bezierCurveTo.call(this,
57951 c1x * xx + dx,
57952 c1y * yy + dy,
57953 c2x * xx + dx,
57954 c2y * yy + dy,
57955 x * xx + dx,
57956 y * yy + dy
57957 );
57958 },
57959
57960 /**
57961 * Returns an object that represents a linear gradient that paints along the line given
57962 * by the coordinates represented by the arguments.
57963 * @return {*}
57964 * @ignore
57965 */
57966 createLinearGradient: function (x0, y0, x1, y1) {
57967 this.updatePrecisionCompensateRect();
57968 var grad = originalCtx.createLinearGradient.call(this,
57969 x0 * xx + dx,
57970 y0 * yy + dy,
57971 x1 * xx + dx,
57972 y1 * yy + dy
57973 );
57974 this.updatePrecisionCompensate();
57975 return grad;
57976 },
57977
57978 /**
57979 * Returns a CanvasGradient object that represents a radial gradient that paints along
57980 * the cone given by the circles represented by the arguments. If either of the radii
57981 * are negative, throws an IndexSizeError exception.
57982 * @return {*}
57983 * @ignore
57984 */
57985 createRadialGradient: function (x0, y0, r0, x1, y1, r1) {
57986 this.updatePrecisionCompensateRect();
57987 var grad = originalCtx.createLinearGradient.call(this,
57988 x0 * xx + dx,
57989 y0 * xx + dy,
57990 r0 * xx,
57991 x1 * xx + dx,
57992 y1 * xx + dy,
57993 r1 * xx
57994 );
57995 this.updatePrecisionCompensate();
57996 return grad;
57997 },
57998
57999 /**
58000 * Fills the given text at the given position. If a maximum width is provided, the text
58001 * will be scaled to fit that width if necessary.
58002 * @ignore
58003 */
58004 fillText: function (text, x, y, maxWidth) {
58005 originalCtx.setTransform.apply(this, matrix.elements);
58006 if (typeof maxWidth === 'undefined') {
58007 originalCtx.fillText.call(this, text, x, y);
58008 } else {
58009 originalCtx.fillText.call(this, text, x, y, maxWidth);
58010 }
58011 this.updatePrecisionCompensate();
58012 },
58013
58014 /**
58015 * Strokes the given text at the given position. If a
58016 * maximum width is provided, the text will be scaled to
58017 * fit that width if necessary.
58018 * @ignore
58019 */
58020 strokeText: function (text, x, y, maxWidth) {
58021 originalCtx.setTransform.apply(this, matrix.elements);
58022 if (typeof maxWidth === 'undefined') {
58023 originalCtx.strokeText.call(this, text, x, y);
58024 } else {
58025 originalCtx.strokeText.call(this, text, x, y, maxWidth);
58026 }
58027 this.updatePrecisionCompensate();
58028 },
58029
58030 /**
58031 * Fills the subpaths of the current default path or the given path with the current fill style.
58032 * @ignore
58033 */
58034 fill: function () {
58035 this.updatePrecisionCompensateRect();
58036 originalCtx.fill.call(this);
58037 this.updatePrecisionCompensate();
58038 },
58039
58040 /**
58041 * Strokes the subpaths of the current default path or the given path with the current stroke style.
58042 * @ignore
58043 */
58044 stroke: function () {
58045 this.updatePrecisionCompensateRect();
58046 originalCtx.stroke.call(this);
58047 this.updatePrecisionCompensate();
58048 },
58049
58050 /**
58051 * Draws the given image onto the canvas. If the first argument isn't an img, canvas,
58052 * or video element, throws a TypeMismatchError exception. If the image has no image
58053 * data, throws an InvalidStateError exception. If the one of the source rectangle
58054 * dimensions is zero, throws an IndexSizeError exception. If the image isn't yet fully
58055 * decoded, then nothing is drawn.
58056 * @return {*}
58057 * @ignore
58058 */
58059 drawImage: function (img_elem, arg1, arg2, arg3, arg4, dst_x, dst_y, dw, dh) {
58060 switch (arguments.length) {
58061 case 3:
58062 return originalCtx.drawImage.call(this, img_elem, arg1 * xx + dx, arg2 * yy + dy);
58063 case 5:
58064 return originalCtx.drawImage.call(this, img_elem, arg1 * xx + dx, arg2 * yy + dy, arg3 * xx, arg4 * yy);
58065 case 9:
58066 return originalCtx.drawImage.call(this, img_elem, arg1, arg2, arg3, arg4, dst_x * xx + dx, dst_y * yy * dy, dw * xx, dh * yy);
58067 }
58068 }
58069 };
58070 Ext.apply(ctx, override);
58071 this.setDirty(true);
58072 },
58073
58074 // Continue docs for the Canvas class
58075 /** @class Ext.draw.engine.Canvas */
58076
58077 updateRegion: function (region) {
58078 this.callSuper([region]);
58079
58080 var me = this,
58081 l = Math.floor(region[0]),
58082 t = Math.floor(region[1]),
58083 r = Math.ceil(region[0] + region[2]),
58084 b = Math.ceil(region[1] + region[3]),
58085 devicePixelRatio = me.devicePixelRatio,
58086 w = r - l,
58087 h = b - t,
58088 splitThreshold = Math.round(me.splitThreshold / devicePixelRatio),
58089 splits = Math.ceil(w / splitThreshold),
58090 activeCanvases = me.activeCanvases,
58091 i, offsetX, dom, leftWidth;
58092
58093 for (i = 0, offsetX = 0; i < splits; i++, offsetX += splitThreshold) {
58094 if (i >= me.canvases.length) {
58095 me.createCanvas();
58096 }
58097 dom = me.canvases[i].dom;
58098 dom.style.left = offsetX + 'px';
58099 if (h * devicePixelRatio !== dom.height) {
58100 dom.height = h * devicePixelRatio;
58101 dom.style.height = h + 'px';
58102 }
58103 leftWidth = Math.min(splitThreshold, w - offsetX);
58104 if (leftWidth * devicePixelRatio !== dom.width) {
58105 dom.width = leftWidth * devicePixelRatio;
58106 dom.style.width = leftWidth + 'px';
58107 }
58108 me.applyDefaults(me.contexts[i]);
58109 }
58110
58111 for (; i < activeCanvases; i++) {
58112 dom = me.canvases[i].dom;
58113 dom.width = 0;
58114 dom.height = 0;
58115 }
58116 me.activeCanvases = splits;
58117 me.clear();
58118 },
58119
58120 /**
58121 * @inheritdoc
58122 */
58123 clearTransform: function () {
58124 var me = this,
58125 activeCanvases = me.activeCanvases,
58126 i, ctx;
58127
58128 for (i = 0; i < activeCanvases; i++) {
58129 ctx = me.contexts[i];
58130 ctx.translate(-me.splitThreshold * i, 0);
58131 ctx.scale(me.devicePixelRatio, me.devicePixelRatio);
58132 me.matrix.toContext(ctx);
58133 }
58134
58135 },
58136
58137 /**
58138 * @private
58139 * @inheritdoc
58140 */
58141 renderSprite: function (sprite) {
58142 var me = this,
58143 region = me._region,
58144 surfaceMatrix = me.matrix,
58145 parent = sprite._parent,
58146 matrix = Ext.draw.Matrix.fly([1, 0, 0, 1, 0, 0]),
58147 bbox, i, offsetX, ctx, width, left = 0, top, right = region[2], bottom;
58148
58149 while (parent && (parent !== me)) {
58150 matrix.prependMatrix(parent.matrix || parent.attr && parent.attr.matrix);
58151 parent = parent.getParent();
58152 }
58153 matrix.prependMatrix(surfaceMatrix);
58154 bbox = sprite.getBBox();
58155 if (bbox) {
58156 bbox = matrix.transformBBox(bbox);
58157 }
58158
58159 sprite.preRender(me);
58160
58161 if (sprite.attr.hidden || sprite.attr.globalAlpha === 0) {
58162 sprite.setDirty(false);
58163 return;
58164 }
58165
58166 top = 0;
58167 bottom = top + region[3];
58168
58169 for (i = 0, offsetX = 0; i < me.activeCanvases; i++, offsetX += me.splitThreshold / me.devicePixelRatio) {
58170 ctx = me.contexts[i];
58171 width = Math.min(region[2] - offsetX, me.splitThreshold / me.devicePixelRatio);
58172 left = offsetX;
58173 right = left + width;
58174
58175 if (bbox) {
58176 if (bbox.x > right ||
58177 bbox.x + bbox.width < left ||
58178 bbox.y > bottom ||
58179 bbox.y + bbox.height < top) {
58180 continue;
58181 }
58182 }
58183
58184 try {
58185 ctx.save();
58186 // Set attributes to context.
58187 sprite.useAttributes(ctx, region);
58188 // Render shape
58189 if (false === sprite.render(me, ctx, [left, top, width, bottom - top], region)) {
58190 return false;
58191 }
58192 } finally {
58193 ctx.restore();
58194 }
58195 }
58196 sprite.setDirty(false);
58197 },
58198
58199 applyDefaults: function (ctx) {
58200 ctx.strokeStyle = 'rgba(0,0,0,0)';
58201 ctx.fillStyle = 'rgba(0,0,0,0)';
58202 ctx.textAlign = 'start';
58203 ctx.textBaseline = 'top';
58204 ctx.miterLimit = 1;
58205 },
58206
58207 /**
58208 * @inheritdoc
58209 */
58210 clear: function () {
58211 var me = this,
58212 activeCanvases = this.activeCanvases,
58213 i, canvas, ctx;
58214 for (i = 0; i < activeCanvases; i++) {
58215 canvas = me.canvases[i].dom;
58216 ctx = me.contexts[i];
58217 ctx.setTransform(1, 0, 0, 1, 0, 0);
58218 ctx.clearRect(0, 0, canvas.width, canvas.height);
58219 }
58220 me.setDirty(true);
58221 },
58222
58223 /**
58224 * Destroys the Canvas element and prepares it for Garbage Collection.
58225 */
58226 destroy: function () {
58227 var me = this,
58228 i, ln = me.canvases.length;
58229 for (i = 0; i < ln; i++) {
58230 me.contexts[i] = null;
58231 me.canvases[i].destroy();
58232 me.canvases[i] = null;
58233 }
58234 delete me.contexts;
58235 delete me.canvases;
58236 me.callSuper(arguments);
58237 }
58238 }, function () {
58239 if (Ext.os.is.Android4 && Ext.browser.is.Chrome) {
58240 this.prototype.splitThreshold = 3000;
58241 } else if (Ext.os.is.Android) {
58242 this.prototype.splitThreshold = 1e10;
58243 }
58244 });
58245
58246 /**
58247 * The Draw Component is a surface in which sprites can be rendered. The Draw Component
58248 * manages and holds a `Surface` instance: an interface that has
58249 * an SVG or VML implementation depending on the browser capabilities and where
58250 * Sprites can be appended.
58251 * One way to create a draw component is:
58252 *
58253 * var drawComponent = new Ext.draw.Component({
58254 * fullscreen: true,
58255 * sprites: [{
58256 * type: 'circle',
58257 * fill: '#79BB3F',
58258 * radius: 100,
58259 * x: 100,
58260 * y: 100
58261 * }]
58262 * });
58263 *
58264 * In this case we created a draw component and added a sprite to it.
58265 * The *type* of the sprite is *circle* so if you run this code you'll see a yellow-ish
58266 * circle in a Window. When setting `viewBox` to `false` we are responsible for setting the object's position and
58267 * dimensions accordingly.
58268 *
58269 * You can also add sprites by using the surface's add method:
58270 *
58271 * drawComponent.getSurface('main').add({
58272 * type: 'circle',
58273 * fill: 'blue',
58274 * radius: 100,
58275 * x: 100,
58276 * y: 100
58277 * });
58278 *
58279 * For more information on Sprites, the core elements added to a draw component's surface,
58280 * refer to the {@link Ext.draw.sprite.Sprite} documentation.
58281 */
58282 Ext.define('Ext.draw.Component', {
58283
58284 extend: Ext.Container ,
58285 xtype: 'draw',
58286 defaultType: 'surface',
58287
58288
58289
58290
58291
58292
58293
58294 engine: 'Ext.draw.engine.Canvas',
58295 statics: {
58296 WATERMARK: 'Powered by <span style="color:#22E962; font-weight: 900">Sencha Touch</span> <span style="color:#75cdff; font-weight: 900">GPLv3</span>'
58297 },
58298 config: {
58299 cls: 'x-draw-component',
58300
58301 /**
58302 * @deprecated 2.2.0 Please implement custom resize event handler.
58303 * Resize the draw component by the content size of the main surface.
58304 *
58305 * __Note:__ It is applied only when there is only one surface.
58306 */
58307 autoSize: false,
58308
58309 /**
58310 * @deprecated 2.2.0 Please implement custom resize event handler.
58311 * Pan/Zoom the content in main surface to fit the component size.
58312 *
58313 * __Note:__ It is applied only when there is only one surface.
58314 */
58315 viewBox: false,
58316
58317 /**
58318 * @deprecated 2.2.0 Please implement custom resize event handler.
58319 * Fit the main surface to the size of component.
58320 *
58321 * __Note:__ It is applied only when there is only one surface.
58322 */
58323 fitSurface: true,
58324
58325 /**
58326 * @cfg {Function} [resizeHandler] The resize function that can be configured to have a behavior.
58327 *
58328 * __Note:__ since resize events trigger {@link #renderFrame} calls automatically,
58329 * return `false` from the resize function, if it also calls `renderFrame`, to prevent double rendering.
58330 */
58331 resizeHandler: null,
58332
58333 background: null,
58334
58335 sprites: null,
58336
58337 /**
58338 * @cfg {Object[]} gradients
58339 * Defines a set of gradients that can be used as color properties
58340 * (fillStyle and strokeStyle, but not shadowColor) in sprites.
58341 * The gradients array is an array of objects with the following properties:
58342 * - **id** - string - The unique name of the gradient.
58343 * - **type** - string, optional - The type of the gradient. Available types are: 'linear', 'radial'. Defaults to 'linear'.
58344 * - **angle** - number, optional - The angle of the gradient in degrees.
58345 * - **stops** - array - An array of objects with 'color' and 'offset' properties, where 'offset' is a real number from 0 to 1.
58346 *
58347 * For example:
58348 *
58349 * gradients: [{
58350 * id: 'gradientId1',
58351 * type: 'linear',
58352 * angle: 45,
58353 * stops: [{
58354 * offset: 0,
58355 * color: 'red'
58356 * }, {
58357 * offset: 1,
58358 * color: 'yellow'
58359 * }]
58360 * }, {
58361 * id: 'gradientId2',
58362 * type: 'radial',
58363 * stops: [{
58364 * offset: 0,
58365 * color: '#555',
58366 * }, {
58367 * offset: 1,
58368 * color: '#ddd',
58369 * }]
58370 * }]
58371 *
58372 * Then the sprites can use 'gradientId1' and 'gradientId2' by setting the color attributes to those ids, for example:
58373 *
58374 * sprite.setAttributes({
58375 * fillStyle: 'url(#gradientId1)',
58376 * strokeStyle: 'url(#gradientId2)'
58377 * });
58378 */
58379 gradients: []
58380 },
58381
58382 constructor: function (config) {
58383 config = config || {};
58384 this.callSuper(arguments);
58385 this.frameCallbackId = Ext.draw.Animator.addFrameCallback('renderFrame', this);
58386 },
58387
58388 applyGradients: function (gradients) {
58389 var result = [],
58390 i, n, gradient, offset;
58391 if (!Ext.isArray(gradients)) {
58392 return result;
58393 }
58394 for (i = 0, n = gradients.length; i < n; i++) {
58395 gradient = gradients[i];
58396 if (!Ext.isObject(gradient)) {
58397 continue;
58398 }
58399 // ExtJS only supported linear gradients, so we didn't have to specify their type
58400 if (typeof gradient.type !== 'string') {
58401 gradient.type = 'linear';
58402 }
58403 if (gradient.angle) {
58404 gradient.degrees = gradient.angle;
58405 delete gradient.angle;
58406 }
58407 // Convert ExtJS stops object to Touch stops array
58408 if (Ext.isObject(gradient.stops)) {
58409 gradient.stops = (function (stops) {
58410 var result = [], stop;
58411 for (offset in stops) {
58412 stop = stops[offset];
58413 stop.offset = offset / 100;
58414 result.push(stop);
58415 }
58416 return result;
58417 })(gradient.stops);
58418 }
58419 result.push(gradient);
58420 }
58421 Ext.draw.gradient.GradientDefinition.add(result);
58422 return result;
58423 },
58424
58425 initialize: function () {
58426 var me = this;
58427 me.callSuper();
58428 me.element.on('resize', 'onResize', this);
58429 },
58430
58431 applySprites: function (sprites) {
58432 // Never update
58433 if (!sprites) {
58434 return;
58435 }
58436
58437 sprites = Ext.Array.from(sprites);
58438
58439 var ln = sprites.length,
58440 i, surface;
58441
58442 for (i = 0; i < ln; i++) {
58443 if (sprites[i].surface instanceof Ext.draw.Surface) {
58444 surface = sprites[i].surface;
58445 } else if (Ext.isString(sprites[i].surface)) {
58446 surface = this.getSurface(sprites[i].surface);
58447 } else {
58448 surface = this.getSurface('main');
58449 }
58450 surface.add(sprites[i]);
58451 }
58452 },
58453
58454 getElementConfig: function () {
58455 return {
58456 reference: 'element',
58457 className: 'x-container',
58458 children: [
58459 {
58460 reference: 'innerElement',
58461 className: 'x-inner',
58462 children: [
58463 {
58464 reference: 'watermarkElement',
58465 cls: 'x-chart-watermark',
58466 html: Ext.draw.Component.WATERMARK,
58467 style: Ext.draw.Component.WATERMARK ? '': 'display: none'
58468 }
58469 ]
58470 }
58471 ]
58472 };
58473 },
58474
58475 updateBackground: function (background) {
58476 this.element.setStyle({
58477 background: background
58478 });
58479 },
58480
58481 /**
58482 * @protected
58483 * Place water mark after resize.
58484 */
58485 onPlaceWatermark: function () {
58486 // Do nothing
58487 },
58488
58489 onResize: function () {
58490 var me = this,
58491 size = me.element.getSize(),
58492 resizeHandler = me.getResizeHandler() || me.resizeHandler,
58493 result;
58494 me.fireEvent('resize', me, size);
58495 result = resizeHandler.call(me, size);
58496 if (result !== false) {
58497 me.renderFrame();
58498 me.onPlaceWatermark();
58499 }
58500 },
58501
58502 resizeHandler: function (size) {
58503 var me = this;
58504
58505
58506 me.getItems().each(function (surface) {
58507 surface.setRegion([0, 0, size.width, size.height]);
58508 });
58509 },
58510
58511 /**
58512 * Get a surface by the given id or create one if it doesn't exist.
58513 * @param {String} [id="main"]
58514 * @return {Ext.draw.Surface}
58515 */
58516 getSurface: function (id) {
58517 id = this.getId() + '-' + (id || 'main');
58518 var me = this,
58519 surfaces = me.getItems(),
58520 surface = surfaces.get(id),
58521 size;
58522
58523 if (!surface) {
58524 surface = me.add({xclass: me.engine, id: id});
58525 if (me.getFitSurface()) {
58526 size = me.element.getSize();
58527 surface.setRegion([0, 0, size.width, size.height]);
58528 }
58529 surface.renderFrame();
58530 }
58531 return surface;
58532 },
58533
58534 /**
58535 * Render all the surfaces in the component.
58536 */
58537 renderFrame: function () {
58538 var me = this,
58539 i, ln, bbox,
58540 surfaces = me.getItems();
58541
58542 for (i = 0, ln = surfaces.length; i < ln; i++) {
58543 surfaces.items[i].renderFrame();
58544 }
58545 },
58546
58547 destroy: function () {
58548 Ext.draw.Animator.removeFrameCallback(this.frameCallbackId);
58549 this.callSuper();
58550 }
58551
58552 }, function () {
58553 if (location.search.match('svg')) {
58554 Ext.draw.Component.prototype.engine = 'Ext.draw.engine.Svg';
58555 } else if ((Ext.os.is.BlackBerry && Ext.os.version.getMajor() === 10) || (Ext.browser.is.AndroidStock4 && (Ext.os.version.getMinor() === 1 || Ext.os.version.getMinor() === 2 || Ext.os.version.getMinor() === 3))) {
58556 // http://code.google.com/p/android/issues/detail?id=37529
58557 Ext.draw.Component.prototype.engine = 'Ext.draw.engine.Svg';
58558 }
58559 });
58560
58561 /**
58562 * @class Ext.chart.Markers
58563 * @extends Ext.draw.sprite.Instancing
58564 *
58565 * Marker sprite. A specialized version of instancing sprite that groups instances.
58566 * Putting a marker is grouped by its category id. Clearing removes that category.
58567 */
58568 Ext.define('Ext.chart.Markers', {
58569 extend: Ext.draw.sprite.Instancing ,
58570 revisions: 0,
58571
58572 constructor: function () {
58573 this.callSuper(arguments);
58574 this.map = {};
58575 this.revisions = {};
58576 },
58577
58578 /**
58579 * Clear the markers in the category
58580 * @param {String} category
58581 */
58582 clear: function (category) {
58583 category = category || 'default';
58584 if (!(category in this.revisions)) {
58585 this.revisions[category] = 1;
58586 } else {
58587 this.revisions[category]++;
58588 }
58589 },
58590
58591 /**
58592 * Put a marker in the category with additional
58593 * attributes.
58594 * @param {String} category
58595 * @param {Object} markerAttr
58596 * @param {String|Number} index
58597 * @param {Boolean} [canonical]
58598 * @param {Boolean} [keepRevision]
58599 */
58600 putMarkerFor: function (category, markerAttr, index, canonical, keepRevision) {
58601 category = category || 'default';
58602
58603 var me = this,
58604 map = me.map[category] || (me.map[category] = {});
58605 if (index in map) {
58606 me.setAttributesFor(map[index], markerAttr, canonical);
58607 } else {
58608 map[index] = me.instances.length;
58609 me.createInstance(markerAttr, null, canonical);
58610 }
58611 me.instances[map[index]].category = category;
58612 if (!keepRevision) {
58613 me.instances[map[index]].revision = me.revisions[category] || (me.revisions[category] = 1);
58614 }
58615 },
58616
58617 /**
58618 *
58619 * @param {String} category
58620 * @param {Mixed} index
58621 * @param {Boolean} [isWithoutTransform]
58622 */
58623 getMarkerBBoxFor: function (category, index, isWithoutTransform) {
58624 if (category in this.map) {
58625 if (index in this.map[category]) {
58626 return this.getBBoxFor(this.map[category][index], isWithoutTransform);
58627 }
58628 }
58629 return {
58630 x: Infinity,
58631 y: Infinity,
58632 width: -Infinity,
58633 height: -Infinity
58634 };
58635 },
58636
58637 getBBox: function () { return null; },
58638
58639 render: function (surface, ctx, clipRegion) {
58640 var me = this,
58641 revisions = me.revisions,
58642 mat = me.attr.matrix,
58643 template = me.getTemplate(),
58644 originalAttr = template.attr,
58645 instances = me.instances,
58646 i, ln = me.instances.length;
58647 mat.toContext(ctx);
58648 template.preRender(surface, ctx, clipRegion);
58649 template.useAttributes(ctx);
58650 for (i = 0; i < ln; i++) {
58651 if (instances[i].hidden || instances[i].revision !== revisions[instances[i].category]) {
58652 continue;
58653 }
58654 ctx.save();
58655 template.attr = instances[i];
58656 template.applyTransformations();
58657 template.useAttributes(ctx);
58658 template.render(surface, ctx, clipRegion);
58659 ctx.restore();
58660 }
58661 template.attr = originalAttr;
58662 }
58663 });
58664
58665 /**
58666 * @class Ext.chart.label.Callout
58667 * @extends Ext.draw.modifier.Modifier
58668 *
58669 * This is a modifier to place labels and callouts by additional attributes.
58670 */
58671 Ext.define("Ext.chart.label.Callout", {
58672 extend: Ext.draw.modifier.Modifier ,
58673
58674 prepareAttributes: function (attr) {
58675 if (!attr.hasOwnProperty('calloutOriginal')) {
58676 attr.calloutOriginal = Ext.Object.chain(attr);
58677 }
58678 if (this._previous) {
58679 this._previous.prepareAttributes(attr.calloutOriginal);
58680 }
58681 },
58682
58683 setAttrs: function (attr, changes) {
58684 var callout = attr.callout,
58685 origin = attr.calloutOriginal,
58686 bbox = attr.bbox.plain,
58687 width = (bbox.width || 0) + attr.labelOverflowPadding,
58688 height = (bbox.height || 0) + attr.labelOverflowPadding,
58689 dx, dy;
58690
58691 if ('callout' in changes) {
58692 callout = changes.callout;
58693 }
58694
58695 if ('callout' in changes || 'calloutPlaceX' in changes || 'calloutPlaceY' in changes || 'x' in changes || 'y' in changes) {
58696 var rotationRads = 'rotationRads' in changes ? origin.rotationRads = changes.rotationRads : origin.rotationRads,
58697 x = 'x' in changes ? (origin.x = changes.x) : origin.x,
58698 y = 'y' in changes ? (origin.y = changes.y) : origin.y,
58699 calloutPlaceX = 'calloutPlaceX' in changes ? changes.calloutPlaceX : attr.calloutPlaceX,
58700 calloutPlaceY = 'calloutPlaceY' in changes ? changes.calloutPlaceY : attr.calloutPlaceY,
58701 calloutVertical = 'calloutVertical' in changes ? changes.calloutVertical : attr.calloutVertical,
58702 temp;
58703
58704 // Normalize Rotations
58705 rotationRads %= Math.PI * 2;
58706 if (Math.cos(rotationRads) < 0) {
58707 rotationRads = (rotationRads + Math.PI) % (Math.PI * 2);
58708 }
58709
58710 if (rotationRads > Math.PI) {
58711 rotationRads -= Math.PI * 2;
58712 }
58713
58714 if (calloutVertical) {
58715 rotationRads = rotationRads * (1 - callout) - Math.PI / 2 * callout;
58716 temp = width;
58717 width = height;
58718 height = temp;
58719 } else {
58720 rotationRads = rotationRads * (1 - callout);
58721 }
58722 changes.rotationRads = rotationRads;
58723
58724
58725 // Placing label.
58726 changes.x = x * (1 - callout) + calloutPlaceX * callout;
58727 changes.y = y * (1 - callout) + calloutPlaceY * callout;
58728
58729
58730 // Placing the end of the callout line.
58731 dx = calloutPlaceX - x;
58732 dy = calloutPlaceY - y;
58733 if (Math.abs(dy * width) > Math.abs(height * dx)) {
58734 // on top/bottom
58735 if (dy > 0) {
58736 changes.calloutEndX = changes.x - (height / (dy * 2) * dx) * callout;
58737 changes.calloutEndY = changes.y - height / 2 * callout;
58738 } else {
58739 changes.calloutEndX = changes.x + (height / (dy * 2) * dx) * callout;
58740 changes.calloutEndY = changes.y + height / 2 * callout;
58741 }
58742 } else {
58743 // on left/right
58744 if (dx > 0) {
58745 changes.calloutEndX = changes.x - width / 2;
58746 changes.calloutEndY = changes.y - (width / (dx * 2) * dy) * callout;
58747 } else {
58748 changes.calloutEndX = changes.x + width / 2;
58749 changes.calloutEndY = changes.y + (width / (dx * 2) * dy) * callout;
58750 }
58751 }
58752 }
58753
58754 return changes;
58755 },
58756
58757 pushDown: function (attr, changes) {
58758 changes = Ext.draw.modifier.Modifier.prototype.pushDown.call(this, attr.calloutOriginal, changes);
58759 return this.setAttrs(attr, changes);
58760 },
58761
58762 popUp: function (attr, changes) {
58763 attr = Object.getPrototypeOf(attr);
58764 changes = this.setAttrs(attr, changes);
58765 if (this._next) {
58766 return this._next.popUp(attr, changes);
58767 } else {
58768 return Ext.apply(attr, changes);
58769 }
58770 }
58771 });
58772
58773 /**
58774 * @class Ext.chart.label.Label
58775 * @extends Ext.draw.sprite.Text
58776 *
58777 * Sprite used to represent labels in series.
58778 */
58779 Ext.define('Ext.chart.label.Label', {
58780 extend: Ext.draw.sprite.Text ,
58781
58782
58783 inheritableStatics: {
58784 def: {
58785 processors: {
58786 callout: 'limited01',
58787 calloutPlaceX: 'number',
58788 calloutPlaceY: 'number',
58789 calloutStartX: 'number',
58790 calloutStartY: 'number',
58791 calloutEndX: 'number',
58792 calloutEndY: 'number',
58793 calloutColor: 'color',
58794 calloutVertical: 'bool',
58795 labelOverflowPadding: 'number',
58796 display: 'enums(none,under,over,rotate,insideStart,insideEnd,outside)',
58797 orientation: 'enums(horizontal,vertical)',
58798 renderer: 'default'
58799 },
58800 defaults: {
58801 callout: 0,
58802 calloutPlaceX: 0,
58803 calloutPlaceY: 0,
58804 calloutStartX: 0,
58805 calloutStartY: 0,
58806 calloutEndX: 0,
58807 calloutEndY: 0,
58808 calloutVertical: false,
58809 calloutColor: 'black',
58810 labelOverflowPadding: 5,
58811 display: 'none',
58812 orientation: '',
58813 renderer: null
58814 },
58815
58816 dirtyTriggers: {
58817 callout: 'transform',
58818 calloutPlaceX: 'transform',
58819 calloutPlaceY: 'transform',
58820 labelOverflowPadding: 'transform',
58821 calloutRotation: 'transform',
58822 display: 'hidden'
58823 },
58824
58825 updaters: {
58826 hidden: function (attrs) {
58827 attrs.hidden = attrs.display === 'none';
58828 }
58829 }
58830 }
58831 },
58832
58833 config: {
58834 /**
58835 * @cfg {Object} fx Animation configuration.
58836 */
58837 fx: {
58838 customDuration: {
58839 callout: 200
58840 }
58841 },
58842 field: null
58843 },
58844
58845 prepareModifiers: function () {
58846 this.callSuper(arguments);
58847 this.calloutModifier = new Ext.chart.label.Callout({sprite: this});
58848 this.fx.setNext(this.calloutModifier);
58849 this.calloutModifier.setNext(this.topModifier);
58850 },
58851
58852 render: function (surface, ctx, clipRegion) {
58853 var me = this,
58854 attr = me.attr;
58855 ctx.save();
58856 ctx.globalAlpha *= Math.max(0, attr.callout - 0.5) * 2;
58857 if (ctx.globalAlpha > 0) {
58858 ctx.strokeStyle = attr.calloutColor;
58859 ctx.fillStyle = attr.calloutColor;
58860 ctx.beginPath();
58861 ctx.moveTo(me.attr.calloutStartX, me.attr.calloutStartY);
58862 ctx.lineTo(me.attr.calloutEndX, me.attr.calloutEndY);
58863 ctx.stroke();
58864
58865 ctx.beginPath();
58866 ctx.arc(me.attr.calloutStartX, me.attr.calloutStartY, 1, 0, 2 * Math.PI, true);
58867 ctx.fill();
58868
58869 ctx.beginPath();
58870 ctx.arc(me.attr.calloutEndX, me.attr.calloutEndY, 1, 0, 2 * Math.PI, true);
58871 ctx.fill();
58872 }
58873 ctx.restore();
58874
58875 Ext.draw.sprite.Text.prototype.render.apply(me, arguments);
58876 }
58877 });
58878
58879 /**
58880 * Series is the abstract class containing the common logic to all chart series. Series includes
58881 * methods from Labels, Highlights, and Callouts mixins. This class implements the logic of
58882 * animating, hiding, showing all elements and returning the color of the series to be used as a legend item.
58883 *
58884 * ## Listeners
58885 *
58886 * The series class supports listeners via the Observable syntax. Some of these listeners are:
58887 *
58888 * - `itemmouseup` When the user interacts with a marker.
58889 * - `itemmousedown` When the user interacts with a marker.
58890 * - `itemmousemove` When the user interacts with a marker.
58891 * - (similar `item*` events occur for many raw mouse and touch events)
58892 * - `afterrender` Will be triggered when the animation ends or when the series has been rendered completely.
58893 *
58894 * For example:
58895 *
58896 * series: [{
58897 * type: 'bar',
58898 * axis: 'left',
58899 * listeners: {
58900 * 'afterrender': function() {
58901 * console('afterrender');
58902 * }
58903 * },
58904 * xField: 'category',
58905 * yField: 'data1'
58906 * }]
58907 *
58908 */
58909 Ext.define('Ext.chart.series.Series', {
58910
58911
58912
58913 mixins: {
58914 observable: Ext.mixin.Observable
58915 },
58916
58917 /**
58918 * @property {String} type
58919 * The type of series. Set in subclasses.
58920 * @protected
58921 */
58922 type: null,
58923
58924 /**
58925 * @property {String} seriesType
58926 * Default series sprite type.
58927 */
58928 seriesType: 'sprite',
58929
58930 identifiablePrefix: 'ext-line-',
58931
58932 observableType: 'series',
58933
58934 /**
58935 * @event chartattached
58936 * Fires when the {@link Ext.chart.AbstractChart} has been attached to this series.
58937 * @param {Ext.chart.AbstractChart} chart
58938 * @param {Ext.chart.series.Series} series
58939 */
58940 /**
58941 * @event chartdetached
58942 * Fires when the {@link Ext.chart.AbstractChart} has been detached from this series.
58943 * @param {Ext.chart.AbstractChart} chart
58944 * @param {Ext.chart.series.Series} series
58945 */
58946
58947 config: {
58948 /**
58949 * @private
58950 * @cfg {Object} chart The chart that the series is bound.
58951 */
58952 chart: null,
58953
58954 /**
58955 * @cfg {String|String[]} title
58956 * The human-readable name of the series (displayed in the legend).
58957 */
58958 title: null,
58959
58960 /**
58961 * @cfg {Function} renderer
58962 * A function that can be provided to set custom styling properties to each rendered element.
58963 * It receives `(sprite, config, rendererData, index)` as parameters.
58964 *
58965 * @param {Object} sprite The sprite affected by the renderer. The visual attributes are in `sprite.attr`.
58966 * The data field is available in `sprite.getField()`.
58967 * @param {Object} config The sprite configuration. It varies with the series and the type of sprite:
58968 * for instance, a Line chart sprite might have just the `x` and `y` properties while a Bar
58969 * chart sprite also has `width` and `height`. A `type` might be present too. For instance to
58970 * draw each marker and each segment of a Line chart, the renderer is called with the
58971 * `config.type` set to either `marker` or `line`.
58972 * @param {Object} rendererData A record with different properties depending on the type of chart.
58973 * The only guaranteed property is `rendererData.store`, the store used by the series.
58974 * In some cases, a store may not exist: for instance a Gauge chart may read its value directly
58975 * from its configuration; in this case rendererData.store is null and the value is
58976 * available in rendererData.value.
58977 * @param {Number} index The index of the sprite. It is usually the index of the store record associated
58978 * with the sprite, in which case the record can be obtained with `store.getData().items[index]`.
58979 * If the chart is not associated with a store, the index represents the index of the sprite within
58980 * the series. For instance a Gauge chart may have as many sprites as there are sectors in the
58981 * background of the gauge, plus one for the needle.
58982 *
58983 * @return {Object} The attributes that have been changed or added. Note: it is usually possible to
58984 * add or modify the attributes directly into the `config` parameter and not return anything,
58985 * but returning an object with only those attributes that have been changed may allow for
58986 * optimizations in the rendering of some series. Example to draw every other item in red:
58987 *
58988 * renderer: function (sprite, config, rendererData, index) {
58989 * if (index % 2 == 0) {
58990 * return { strokeStyle: 'red' };
58991 * }
58992 * }
58993 */
58994 renderer: null,
58995
58996 /**
58997 * @cfg {Boolean} showInLegend
58998 * Whether to show this series in the legend.
58999 */
59000 showInLegend: true,
59001
59002 //@private triggerdrawlistener flag
59003 triggerAfterDraw: false,
59004
59005 /**
59006 * @private
59007 * Not supported.
59008 */
59009 themeStyle: {},
59010
59011 /**
59012 * @cfg {Object} style Custom style configuration for the sprite used in the series.
59013 */
59014 style: {},
59015
59016 /**
59017 * @cfg {Object} subStyle This is the cyclic used if the series has multiple sprites.
59018 */
59019 subStyle: {},
59020
59021 /**
59022 * @cfg {Array} colors
59023 * An array of color values which will be used, in order, as the pie slice fill colors.
59024 */
59025 colors: null,
59026
59027 /**
59028 * @protected
59029 * @cfg {Object} store The store of values used in the series.
59030 */
59031 store: null,
59032
59033 /**
59034 * @cfg {Object} label
59035 * The style object for labels.
59036 */
59037
59038 /**
59039 * @cfg {Object} label
59040 * Object with the following properties:
59041 *
59042 * @cfg {String} label.display
59043 *
59044 * Specifies the presence and position of the labels. The possible values depend on the chart type.
59045 * For Line charts: 'under' | 'over' | 'rotate'.
59046 * For Bar charts: 'insideStart' | 'insideEnd' | 'outside'.
59047 * For Pie charts: 'outside' | 'rotate'.
59048 * For all charts: 'none' hides the labels.
59049 *
59050 * Default value: 'none'.
59051 *
59052 * @cfg {String} label.color
59053 *
59054 * The color of the label text.
59055 *
59056 * Default value: '#000' (black).
59057 *
59058 * @cfg {String|String[]} label.field
59059 *
59060 * The name(s) of the field(s) to be displayed in the labels. If your chart has 3 series
59061 * that correspond to the fields 'a', 'b', and 'c' of your model and you only want to
59062 * display labels for the series 'c', you must still provide an array `[null, null, 'c']`.
59063 *
59064 * Default value: null.
59065 *
59066 * @cfg {String} label.font
59067 *
59068 * The font used for the labels.
59069 *
59070 * Default value: '14px Helvetica'.
59071 *
59072 * @cfg {String} label.orientation
59073 *
59074 * Either 'horizontal' or 'vertical'. If not set (default), the orientation is inferred
59075 * from the value of the flipXY property of the series.
59076 *
59077 * Default value: ''.
59078 *
59079 * @cfg {Function} label.renderer
59080 *
59081 * Optional function for formatting the label into a displayable value.
59082 *
59083 * The arguments to the method are:
59084 *
59085 * - *`text`*, *`sprite`*, *`config`*, *`rendererData`*, *`index`*
59086 *
59087 * Label's renderer is passed the same arguments as {@link #renderer}
59088 * plus one extra 'text' argument which comes first.
59089 *
59090 * @return {Object|String} The attributes that have been changed or added, or the text for the label.
59091 * Example to enclose every other label in parentheses:
59092 *
59093 * renderer: function (text) {
59094 * if (index % 2 == 0) {
59095 * return '(' + text + ')'
59096 * }
59097 * }
59098 *
59099 * Default value: null.
59100 */
59101 label: {textBaseline: 'middle', textAlign: 'center', font: '14px Helvetica'},
59102
59103 /**
59104 * @cfg {Number} labelOverflowPadding
59105 * Extra distance value for which the labelOverflow listener is triggered.
59106 */
59107 labelOverflowPadding: 5,
59108
59109 /**
59110 * @cfg {String|String[]} labelField
59111 * @deprecated Use 'field' property of {@link Ext.chart.series.Series#label} instead.
59112 * The store record field name to be used for the series labels.
59113 */
59114 labelField: null,
59115
59116 /**
59117 * @cfg {Object} marker
59118 * The sprite template used by marker instances on the series.
59119 */
59120 marker: null,
59121
59122 /**
59123 * @cfg {Object} markerSubStyle
59124 * This is cyclic used if series have multiple marker sprites.
59125 */
59126 markerSubStyle: null,
59127
59128 /**
59129 * @protected
59130 * @cfg {Object} itemInstancing The sprite template used to create sprite instances in the series.
59131 */
59132 itemInstancing: null,
59133
59134 /**
59135 * @cfg {Object} background Sets the background of the surface the series is attached.
59136 */
59137 background: null,
59138
59139 /**
59140 * @cfg {Object} highlightItem The item currently highlighted in the series.
59141 */
59142 highlightItem: null,
59143
59144 /**
59145 * @protected
59146 * @cfg {Object} surface The surface that the series is attached.
59147 */
59148 surface: null,
59149
59150 /**
59151 * @protected
59152 * @cfg {Object} overlaySurface The surface that series markers are attached.
59153 */
59154 overlaySurface: null,
59155
59156 /**
59157 * @cfg {Boolean|Array} hidden
59158 */
59159 hidden: false,
59160
59161 /**
59162 * @cfg {Object} highlightCfg The sprite configuration used when highlighting items in the series.
59163 */
59164 highlightCfg: null,
59165
59166 /**
59167 * @cfg {Object} animate The series animation configuration.
59168 */
59169 animate: null
59170 },
59171
59172 directions: [],
59173
59174 sprites: null,
59175
59176 getFields: function (fieldCategory) {
59177 var me = this,
59178 fields = [], fieldsItem,
59179 i, ln;
59180 for (i = 0, ln = fieldCategory.length; i < ln; i++) {
59181 fieldsItem = me['get' + fieldCategory[i] + 'Field']();
59182 fields.push(fieldsItem);
59183 }
59184 return fields;
59185 },
59186
59187 updateAnimate: function (animate) {
59188 var sprites = this.getSprites(), i = -1, ln = sprites.length;
59189 while (++i < ln) {
59190 sprites[i].fx.setConfig(animate);
59191 }
59192 },
59193
59194 updateTitle: function (newTitle) {
59195 if (newTitle) {
59196 var chart = this.getChart(),
59197 series = chart.getSeries(),
59198 legendStore = chart.getLegendStore(),
59199 index, rec;
59200
59201 if (series) {
59202 index = Ext.Array.indexOf(series, this);
59203
59204 if (index !== -1) {
59205 rec = legendStore.getAt(index);
59206 rec.set('name', newTitle);
59207 }
59208 }
59209 }
59210 },
59211
59212 updateColors: function (colorSet) {
59213 this.setSubStyle({fillStyle: colorSet});
59214 this.doUpdateStyles();
59215 },
59216
59217 applyHighlightCfg: function (highlight, oldHighlight) {
59218 return Ext.apply(oldHighlight || {}, highlight);
59219 },
59220
59221 applyItemInstancing: function (instancing, oldInstancing) {
59222 return Ext.merge(oldInstancing || {}, instancing);
59223 },
59224
59225 setAttributesForItem: function (item, change) {
59226 if (item && item.sprite) {
59227 if (item.sprite.itemsMarker && item.category === 'items') {
59228 item.sprite.putMarker(item.category, change, item.index, false, true);
59229 }
59230 if (item.sprite.isMarkerHolder && item.category === 'markers') {
59231 item.sprite.putMarker(item.category, change, item.index, false, true);
59232 } else if (item.sprite instanceof Ext.draw.sprite.Instancing) {
59233 item.sprite.setAttributesFor(item.index, change);
59234 } else {
59235
59236 item.sprite.setAttributes(change);
59237 }
59238 }
59239 },
59240
59241 applyHighlightItem: function (newHighlightItem, oldHighlightItem) {
59242 if (newHighlightItem === oldHighlightItem) {
59243 return;
59244 }
59245 if (Ext.isObject(newHighlightItem) && Ext.isObject(oldHighlightItem)) {
59246 if (newHighlightItem.sprite === oldHighlightItem.sprite &&
59247 newHighlightItem.index === oldHighlightItem.index
59248 ) {
59249 return;
59250 }
59251 }
59252 return newHighlightItem;
59253 },
59254
59255 updateHighlightItem: function (newHighlightItem, oldHighlightItem) {
59256 this.setAttributesForItem(oldHighlightItem, {highlighted: false});
59257 this.setAttributesForItem(newHighlightItem, {highlighted: true});
59258 },
59259
59260 constructor: function (config) {
59261 var me = this;
59262 me.getId();
59263 me.sprites = [];
59264 me.dataRange = [];
59265 Ext.ComponentManager.register(me);
59266 me.mixins.observable.constructor.apply(me, arguments);
59267 },
59268
59269 applyStore: function (store) {
59270 return Ext.StoreManager.lookup(store);
59271 },
59272
59273 getStore: function () {
59274 return this._store || this.getChart() && this.getChart().getStore();
59275 },
59276
59277 updateStore: function (newStore, oldStore) {
59278 var me = this,
59279 chartStore = this.getChart() && this.getChart().getStore(),
59280 sprites = me.getSprites(),
59281 ln = sprites.length,
59282 i, sprite;
59283 newStore = newStore || chartStore;
59284 oldStore = oldStore || chartStore;
59285
59286 if (oldStore) {
59287 oldStore.un('updaterecord', 'onUpdateRecord', me);
59288 oldStore.un('refresh', 'refresh', me);
59289 }
59290 if (newStore) {
59291 newStore.on('updaterecord', 'onUpdateRecord', me);
59292 newStore.on('refresh', 'refresh', me);
59293 for (i = 0; i < ln; i++) {
59294 sprite = sprites[i];
59295 if (sprite.setStore) {
59296 sprite.setStore(newStore);
59297 }
59298 }
59299 me.refresh();
59300 }
59301 },
59302
59303 onStoreChanged: function (store, oldStore) {
59304 if (!this._store) {
59305 this.updateStore(store, oldStore);
59306 }
59307 },
59308
59309 coordinateStacked: function (direction, directionOffset, directionCount) {
59310 var me = this,
59311 store = me.getStore(),
59312 items = store.getData().items,
59313 axis = me['get' + direction + 'Axis'](),
59314 hidden = me.getHidden(),
59315 range = {min: 0, max: 0},
59316 directions = me['fieldCategory' + direction],
59317 fieldCategoriesItem,
59318 i, j, k, fields, field, data, style = {},
59319 dataStart = [], dataEnd, posDataStart = [], negDataStart = [],
59320 stacked = me.getStacked(),
59321 sprites = me.getSprites();
59322
59323 if (sprites.length > 0) {
59324 for (i = 0; i < directions.length; i++) {
59325 fieldCategoriesItem = directions[i];
59326 fields = me.getFields([fieldCategoriesItem]);
59327 for (j = 0; j < items.length; j++) {
59328 dataStart[j] = 0;
59329 posDataStart[j] = 0;
59330 negDataStart[j] = 0;
59331 }
59332 for (j = 0; j < fields.length; j++) {
59333 style = {};
59334 field = fields[j];
59335 if (hidden[j]) {
59336 style['dataStart' + fieldCategoriesItem] = dataStart;
59337 style['data' + fieldCategoriesItem] = dataStart;
59338 sprites[j].setAttributes(style);
59339 continue;
59340 }
59341 data = me.coordinateData(items, field, axis);
59342 if (stacked) {
59343 dataEnd = [];
59344 for (k = 0; k < items.length; k++) {
59345 if (!data[k]) {
59346 data[k] = 0;
59347 }
59348 if (data[k] >= 0) {
59349 dataStart[k] = posDataStart[k];
59350 posDataStart[k] += data[k];
59351 dataEnd[k] = posDataStart[k];
59352 } else {
59353 dataStart[k] = negDataStart[k];
59354 negDataStart[k] += data[k];
59355 dataEnd[k] = negDataStart[k];
59356 }
59357 }
59358 style['dataStart' + fieldCategoriesItem] = dataStart;
59359 style['data' + fieldCategoriesItem] = dataEnd;
59360 me.getRangeOfData(dataStart, range);
59361 me.getRangeOfData(dataEnd, range);
59362 } else {
59363 style['dataStart' + fieldCategoriesItem] = dataStart;
59364 style['data' + fieldCategoriesItem] = data;
59365 me.getRangeOfData(data, range);
59366 }
59367 sprites[j].setAttributes(style);
59368 }
59369 }
59370 me.dataRange[directionOffset] = range.min;
59371 me.dataRange[directionOffset + directionCount] = range.max;
59372 style = {};
59373 style['dataMin' + direction] = range.min;
59374 style['dataMax' + direction] = range.max;
59375 for (i = 0; i < sprites.length; i++) {
59376 sprites[i].setAttributes(style);
59377 }
59378 }
59379 },
59380
59381 coordinate: function (direction, directionOffset, directionCount) {
59382 var me = this,
59383 store = me.getStore(),
59384 hidden = me.getHidden(),
59385 items = store.getData().items,
59386 axis = me['get' + direction + 'Axis'](),
59387 range = {min: Infinity, max: -Infinity},
59388 fieldCategory = me['fieldCategory' + direction] || [direction],
59389 fields = me.getFields(fieldCategory),
59390 i, field, data, style = {},
59391 sprites = me.getSprites();
59392 if (sprites.length > 0) {
59393 if (!Ext.isBoolean(hidden) || !hidden) {
59394 for (i = 0; i < fieldCategory.length; i++) {
59395 field = fields[i];
59396 data = me.coordinateData(items, field, axis);
59397 me.getRangeOfData(data, range);
59398 style['data' + fieldCategory[i]] = data;
59399 }
59400 }
59401 me.dataRange[directionOffset] = range.min;
59402 me.dataRange[directionOffset + directionCount] = range.max;
59403 style['dataMin' + direction] = range.min;
59404 style['dataMax' + direction] = range.max;
59405 if (axis) {
59406 axis.range = null;
59407 style['range' + direction] = axis.getRange();
59408 }
59409 for (i = 0; i < sprites.length; i++) {
59410 sprites[i].setAttributes(style);
59411 }
59412 }
59413 },
59414
59415 /**
59416 * @private
59417 * This method will return an array containing data coordinated by a specific axis.
59418 * @param {Array} items
59419 * @param {String} field
59420 * @param {Ext.chart.axis.Axis} axis
59421 * @return {Array}
59422 */
59423 coordinateData: function (items, field, axis) {
59424 var data = [],
59425 length = items.length,
59426 layout = axis && axis.getLayout(),
59427 coord = axis ? function (x, field, idx, items) {
59428 return layout.getCoordFor(x, field, idx, items);
59429 } : function (x) { return +x; },
59430 i, x;
59431 for (i = 0; i < length; i++) {
59432 x = items[i].data[field];
59433 data[i] = !Ext.isEmpty(x) ? coord(x, field, i, items) : x;
59434 }
59435 return data;
59436 },
59437
59438 getRangeOfData: function (data, range) {
59439 var i, length = data.length,
59440 value, min = range.min, max = range.max;
59441 for (i = 0; i < length; i++) {
59442 value = data[i];
59443 if (value < min) {
59444 min = value;
59445 }
59446 if (value > max) {
59447 max = value;
59448 }
59449 }
59450 range.min = min;
59451 range.max = max;
59452 },
59453
59454 updateLabelData: function () {
59455 var me = this,
59456 store = me.getStore(),
59457 items = store.getData().items,
59458 sprites = me.getSprites(),
59459 labelTpl = me.getLabel().getTemplate(),
59460 labelFields = Ext.Array.from(labelTpl.getField() || me.getLabelField()),
59461 i, j, ln, labels,
59462 sprite, field;
59463
59464 if (!sprites.length || !labelFields.length) {
59465 return;
59466 }
59467
59468 for (i = 0; i < sprites.length; i++) {
59469 labels = [];
59470 sprite = sprites[i];
59471 field = sprite.getField();
59472 if (labelFields.indexOf(field) < 0) {
59473 field = labelFields[i];
59474 }
59475 for (j = 0, ln = items.length; j < ln; j++) {
59476 labels.push(items[j].get(field));
59477 }
59478 sprite.setAttributes({labels: labels});
59479 }
59480 },
59481
59482 updateLabelField: function (labelField) {
59483 var labelTpl = this.getLabel().getTemplate();
59484 if (!labelTpl.config.field) {
59485 labelTpl.setField(labelField)
59486 }
59487 },
59488
59489 processData: function () {
59490 if (!this.getStore()) {
59491 return;
59492 }
59493
59494 var me = this,
59495 directions = this.directions,
59496 i, ln = directions.length,
59497 fieldCategory, axis;
59498
59499 for (i = 0; i < ln; i++) {
59500 fieldCategory = directions[i];
59501 if (me['get' + fieldCategory + 'Axis']) {
59502 axis = me['get' + fieldCategory + 'Axis']();
59503 if (axis) {
59504 axis.processData(me);
59505 continue;
59506 }
59507 }
59508 if (me['coordinate' + fieldCategory]) {
59509 me['coordinate' + fieldCategory]();
59510 }
59511 }
59512 me.updateLabelData();
59513 },
59514
59515 applyBackground: function (background) {
59516 if (this.getChart()) {
59517 this.getSurface().setBackground(background);
59518 return this.getSurface().getBackground();
59519 } else {
59520 return background;
59521 }
59522 },
59523
59524 updateChart: function (newChart, oldChart) {
59525 var me = this;
59526 if (oldChart) {
59527 oldChart.un('axeschanged', 'onAxesChanged', me);
59528 // TODO: destroy them
59529 me.sprites = [];
59530 me.setSurface(null);
59531 me.setOverlaySurface(null);
59532 me.onChartDetached(oldChart);
59533 }
59534 if (newChart) {
59535 me.setSurface(newChart.getSurface('series-surface', 'series'));
59536 me.setOverlaySurface(newChart.getSurface('overlay-surface', 'overlay'));
59537
59538 newChart.on('axeschanged', 'onAxesChanged', me);
59539 if (newChart.getAxes()) {
59540 me.onAxesChanged(newChart);
59541 }
59542 me.onChartAttached(newChart);
59543 }
59544
59545 me.updateStore(me._store, null);
59546 },
59547
59548 onAxesChanged: function (chart) {
59549 var me = this,
59550 axes = chart.getAxes(), axis,
59551 directionMap = {}, directionMapItem,
59552 fieldMap = {}, fieldMapItem,
59553 needHighPrecision = false,
59554 directions = this.directions, direction,
59555 i, ln, j, ln2, k, ln3;
59556
59557 for (i = 0, ln = directions.length; i < ln; i++) {
59558 direction = directions[i];
59559 fieldMap[direction] = me.getFields(me['fieldCategory' + direction]);
59560 }
59561
59562 for (i = 0, ln = axes.length; i < ln; i++) {
59563 axis = axes[i];
59564 if (!directionMap[axis.getDirection()]) {
59565 directionMap[axis.getDirection()] = [axis];
59566 } else {
59567 directionMap[axis.getDirection()].push(axis);
59568 }
59569 }
59570
59571 for (i = 0, ln = directions.length; i < ln; i++) {
59572 direction = directions[i];
59573 if (directionMap[direction]) {
59574 directionMapItem = directionMap[direction];
59575 for (j = 0, ln2 = directionMapItem.length; j < ln2; j++) {
59576 axis = directionMapItem[j];
59577 if (axis.getFields().length === 0) {
59578 me['set' + direction + 'Axis'](axis);
59579 if (axis.getNeedHighPrecision()) {
59580 needHighPrecision = true;
59581 }
59582 } else {
59583 fieldMapItem = fieldMap[direction];
59584 if (fieldMapItem) {
59585 for (k = 0, ln3 = fieldMapItem.length; k < ln3; k++) {
59586 if (axis.fieldsMap[fieldMapItem[k]]) {
59587 me['set' + direction + 'Axis'](axis);
59588 if (axis.getNeedHighPrecision()) {
59589 needHighPrecision = true;
59590 }
59591 break;
59592 }
59593 }
59594 }
59595 }
59596 }
59597 }
59598 }
59599 this.getSurface().setHighPrecision(needHighPrecision);
59600 },
59601
59602 onChartDetached: function (oldChart) {
59603 var me = this;
59604 me.fireEvent('chartdetached', oldChart, me);
59605 oldChart.un('storechanged', 'onStoreChanged', me);
59606 },
59607
59608 onChartAttached: function (chart) {
59609 var me = this;
59610 me.setBackground(me.getBackground());
59611 me.fireEvent('chartattached', chart, me);
59612 chart.on('storechanged', 'onStoreChanged', me);
59613 me.processData();
59614 },
59615
59616 updateOverlaySurface: function (overlaySurface) {
59617 var me = this;
59618 if (overlaySurface) {
59619 if (me.getLabel()) {
59620 me.getOverlaySurface().add(me.getLabel());
59621 }
59622 }
59623 },
59624
59625 applyLabel: function (newLabel, oldLabel) {
59626 if (!oldLabel) {
59627 oldLabel = new Ext.chart.Markers({zIndex: 10});
59628 oldLabel.setTemplate(new Ext.chart.label.Label(newLabel));
59629 } else {
59630 oldLabel.getTemplate().setAttributes(newLabel);
59631 }
59632 return oldLabel;
59633 },
59634
59635 createItemInstancingSprite: function (sprite, itemInstancing) {
59636 var me = this,
59637 template,
59638 markers = new Ext.chart.Markers();
59639
59640 markers.setAttributes({zIndex: Number.MAX_VALUE});
59641 var config = Ext.apply({}, itemInstancing);
59642 if (me.getHighlightCfg()) {
59643 config.highlightCfg = me.getHighlightCfg();
59644 config.modifiers = ['highlight'];
59645 }
59646 markers.setTemplate(config);
59647 template = markers.getTemplate();
59648 template.setAttributes(me.getStyle());
59649 template.fx.on('animationstart', 'onSpriteAnimationStart', this);
59650 template.fx.on('animationend', 'onSpriteAnimationEnd', this);
59651 sprite.bindMarker('items', markers);
59652
59653 me.getSurface().add(markers);
59654 return markers;
59655 },
59656
59657 getDefaultSpriteConfig: function () {
59658 return {
59659 type: this.seriesType,
59660 renderer: this.getRenderer()
59661 };
59662 },
59663
59664 createSprite: function () {
59665 var me = this,
59666 surface = me.getSurface(),
59667 itemInstancing = me.getItemInstancing(),
59668 marker, config,
59669 sprite = surface.add(me.getDefaultSpriteConfig());
59670
59671 sprite.setAttributes(this.getStyle());
59672
59673 if (itemInstancing) {
59674 sprite.itemsMarker = me.createItemInstancingSprite(sprite, itemInstancing);
59675 }
59676
59677 if (sprite.bindMarker) {
59678 if (me.getMarker()) {
59679 marker = new Ext.chart.Markers();
59680 config = Ext.merge({}, me.getMarker());
59681 if (me.getHighlightCfg()) {
59682 config.highlightCfg = me.getHighlightCfg();
59683 config.modifiers = ['highlight'];
59684 }
59685 marker.setTemplate(config);
59686 marker.getTemplate().fx.setCustomDuration({
59687 translationX: 0,
59688 translationY: 0
59689 });
59690 sprite.dataMarker = marker;
59691 sprite.bindMarker('markers', marker);
59692 me.getOverlaySurface().add(marker);
59693 }
59694 if (me.getLabel().getTemplate().getField() || me.getLabelField()) {
59695 sprite.bindMarker('labels', me.getLabel());
59696 }
59697 }
59698
59699 if (sprite.setStore) {
59700 sprite.setStore(me.getStore());
59701 }
59702
59703 sprite.fx.on('animationstart', 'onSpriteAnimationStart', me);
59704 sprite.fx.on('animationend', 'onSpriteAnimationEnd', me);
59705
59706 me.sprites.push(sprite);
59707
59708 return sprite;
59709 },
59710
59711 /**
59712 * Performs drawing of this series.
59713 */
59714 getSprites: Ext.emptyFn,
59715
59716 onUpdateRecord: function () {
59717 // TODO: do something REALLY FAST.
59718 this.processData();
59719 },
59720
59721 refresh: function () {
59722 this.processData();
59723 },
59724
59725 isXType: function (xtype) {
59726 return xtype === 'series';
59727 },
59728
59729 getItemId: function () {
59730 return this.getId();
59731 },
59732
59733 applyStyle: function (style, oldStyle) {
59734 // TODO: Incremental setter
59735 var cls = Ext.ClassManager.get(Ext.ClassManager.getNameByAlias('sprite.' + this.seriesType));
59736 if (cls && cls.def) {
59737 style = cls.def.normalize(style);
59738 }
59739 return Ext.apply(oldStyle || Ext.Object.chain(this.getThemeStyle()), style);
59740 },
59741
59742 applyMarker: function (marker, oldMarker) {
59743 var type = (marker && marker.type) || (oldMarker && oldMarker.type) || this.seriesType,
59744 cls;
59745 if (type) {
59746 cls = Ext.ClassManager.get(Ext.ClassManager.getNameByAlias('sprite.' + type));
59747 if (cls && cls.def) {
59748 marker = cls.def.normalize(marker, true);
59749 marker.type = type;
59750 return Ext.merge(oldMarker || {}, marker);
59751 }
59752 return Ext.merge(oldMarker || {}, marker);
59753 }
59754 },
59755
59756 applyMarkerSubStyle: function (marker, oldMarker) {
59757 var type = (marker && marker.type) || (oldMarker && oldMarker.type) || this.seriesType,
59758 cls;
59759 if (type) {
59760 cls = Ext.ClassManager.get(Ext.ClassManager.getNameByAlias('sprite.' + type));
59761 if (cls && cls.def) {
59762 marker = cls.def.batchedNormalize(marker, true);
59763 return Ext.merge(oldMarker || {}, marker);
59764 }
59765 return Ext.merge(oldMarker || {}, marker);
59766 }
59767 },
59768
59769 applySubStyle: function (subStyle, oldSubStyle) {
59770 var cls = Ext.ClassManager.get(Ext.ClassManager.getNameByAlias('sprite.' + this.seriesType));
59771 if (cls && cls.def) {
59772 subStyle = cls.def.batchedNormalize(subStyle, true);
59773 return Ext.merge(oldSubStyle || {}, subStyle);
59774 }
59775 return Ext.merge(oldSubStyle || {}, subStyle);
59776 },
59777
59778 updateHidden: function (hidden) {
59779 // TODO: remove this when jacky fix the problem.
59780 this.getColors();
59781 this.getSubStyle();
59782 this.setSubStyle({hidden: hidden});
59783 this.processData();
59784 this.doUpdateStyles();
59785 },
59786
59787 /**
59788 *
59789 * @param {Number} index
59790 * @param {Boolean} value
59791 */
59792 setHiddenByIndex: function (index, value) {
59793 if (Ext.isArray(this.getHidden())) {
59794 this.getHidden()[index] = value;
59795 this.updateHidden(this.getHidden());
59796 } else {
59797 this.setHidden(value);
59798 }
59799 },
59800
59801 updateStyle: function () {
59802 this.doUpdateStyles();
59803 },
59804
59805 updateSubStyle: function () {
59806 this.doUpdateStyles();
59807 },
59808
59809 doUpdateStyles: function () {
59810 var sprites = this.sprites,
59811 itemInstancing = this.getItemInstancing(),
59812 i = 0, ln = sprites && sprites.length,
59813 markerCfg = this.getMarker(),
59814 style;
59815 for (; i < ln; i++) {
59816 style = this.getStyleByIndex(i);
59817 if (itemInstancing) {
59818 sprites[i].itemsMarker.getTemplate().setAttributes(style);
59819 }
59820 sprites[i].setAttributes(style);
59821 if (markerCfg && sprites[i].dataMarker) {
59822 sprites[i].dataMarker.getTemplate().setAttributes(this.getMarkerStyleByIndex(i));
59823 }
59824 }
59825 },
59826
59827 getMarkerStyleByIndex: function (i) {
59828 return this.getOverriddenStyleByIndex(i, this.getOverriddenStyleByIndex(i, this.getMarkerSubStyle(), this.getMarker()), this.getStyleByIndex(i));
59829 },
59830
59831 getStyleByIndex: function (i) {
59832 return this.getOverriddenStyleByIndex(i, this.getSubStyle(), this.getStyle());
59833 },
59834
59835 getOverriddenStyleByIndex: function (i, subStyle, baseStyle) {
59836 var subStyleItem,
59837 result = Ext.Object.chain(baseStyle || {});
59838 for (var name in subStyle) {
59839 subStyleItem = subStyle[name];
59840 if (Ext.isArray(subStyleItem)) {
59841 result[name] = subStyleItem[i % subStyle[name].length];
59842 } else {
59843 result[name] = subStyleItem;
59844 }
59845 }
59846 return result;
59847 },
59848
59849 /**
59850 * For a given x/y point relative to the main region, find a corresponding item from this
59851 * series, if any.
59852 * @param {Number} x
59853 * @param {Number} y
59854 * @param {Object} [target] optional target to receive the result
59855 * @return {Object} An object describing the item, or null if there is no matching item. The exact contents of
59856 * this object will vary by series type, but should always contain at least the following:
59857 *
59858 * @return {Ext.data.Model} return.record the record of the item.
59859 * @return {Array} return.point the x/y coordinates relative to the chart box of a single point
59860 * for this data item, which can be used as e.g. a tooltip anchor point.
59861 * @return {Ext.draw.sprite.Sprite} return.sprite the item's rendering Sprite.
59862 * @return {Number} return.subSprite the index if sprite is an instancing sprite.
59863 */
59864 getItemForPoint: Ext.emptyFn,
59865
59866 onSpriteAnimationStart: function (sprite) {
59867 this.fireEvent('animationstart', sprite);
59868 },
59869
59870 onSpriteAnimationEnd: function (sprite) {
59871 this.fireEvent('animationend', sprite);
59872 },
59873
59874 /**
59875 * Provide legend information to target array.
59876 *
59877 * @param {Array} target
59878 *
59879 * The information consists:
59880 * @param {String} target.name
59881 * @param {String} target.markColor
59882 * @param {Boolean} target.disabled
59883 * @param {String} target.series
59884 * @param {Number} target.index
59885 */
59886 provideLegendInfo: function (target) {
59887 target.push({
59888 name: this.getTitle() || this.getId(),
59889 mark: 'black',
59890 disabled: false,
59891 series: this.getId(),
59892 index: 0
59893 });
59894 },
59895
59896 destroy: function () {
59897 this.clearListeners();
59898 Ext.ComponentManager.unregister(this);
59899 var store = this.getStore();
59900 if (store && store.getAutoDestroy()) {
59901 Ext.destroy(store);
59902 }
59903 this.setStore(null);
59904 this.callSuper();
59905 }
59906 });
59907
59908 /**
59909 * @class Ext.chart.interactions.Abstract
59910 *
59911 * Defines a common abstract parent class for all interactions.
59912 *
59913 */
59914 Ext.define('Ext.chart.interactions.Abstract', {
59915
59916 xtype: 'interaction',
59917
59918 mixins: {
59919 observable: Ext.mixin.Observable
59920 },
59921
59922 config: {
59923 /**
59924 * @cfg {String} gesture
59925 * Specifies which gesture type should be used for starting the interaction.
59926 */
59927 gesture: 'tap',
59928
59929 /**
59930 * @cfg {Ext.chart.AbstractChart} chart The chart that the interaction is bound.
59931 */
59932 chart: null,
59933
59934 /**
59935 * @cfg {Boolean} enabled 'true' if the interaction is enabled.
59936 */
59937 enabled: true
59938 },
59939
59940 /**
59941 * Android device is emerging too many events so if we re-render every frame it will take for-ever to finish a frame.
59942 * This throttle technique will limit the timespan between two frames.
59943 */
59944 throttleGap: 0,
59945
59946 stopAnimationBeforeSync: false,
59947
59948 constructor: function (config) {
59949 var me = this;
59950 me.initConfig(config);
59951 Ext.ComponentManager.register(this);
59952 },
59953
59954 /**
59955 * @protected
59956 * A method to be implemented by subclasses where all event attachment should occur.
59957 */
59958 initialize: Ext.emptyFn,
59959
59960 updateChart: function (newChart, oldChart) {
59961 var me = this, gestures = me.getGestures();
59962 if (oldChart) {
59963 me.removeChartListener(oldChart);
59964 }
59965 if (newChart) {
59966 me.addChartListener();
59967 }
59968 },
59969
59970 updateEnabled: function (enabled) {
59971 var me = this,
59972 chart = me.getChart();
59973 if (chart) {
59974 if (enabled) {
59975 me.addChartListener();
59976 } else {
59977 me.removeChartListener(chart);
59978 }
59979 }
59980 },
59981
59982 getGestures: function () {
59983 var gestures = {};
59984 gestures[this.getGesture()] = this.onGesture;
59985 return gestures;
59986 },
59987
59988 /**
59989 * @protected
59990 * Placeholder method.
59991 */
59992 onGesture: Ext.emptyFn,
59993
59994 /**
59995 * @protected Find and return a single series item corresponding to the given event,
59996 * or null if no matching item is found.
59997 * @param {Event} e
59998 * @return {Object} the item object or null if none found.
59999 */
60000 getItemForEvent: function (e) {
60001 var me = this,
60002 chart = me.getChart(),
60003 chartXY = chart.getEventXY(e);
60004 return chart.getItemForPoint(chartXY[0], chartXY[1]);
60005 },
60006
60007 /**
60008 * @protected Find and return all series items corresponding to the given event.
60009 * @param {Event} e
60010 * @return {Array} array of matching item objects
60011 */
60012 getItemsForEvent: function (e) {
60013 var me = this,
60014 chart = me.getChart(),
60015 chartXY = chart.getEventXY(e);
60016 return chart.getItemsForPoint(chartXY[0], chartXY[1]);
60017 },
60018
60019 /**
60020 * @private
60021 */
60022 addChartListener: function () {
60023 var me = this,
60024 chart = me.getChart(),
60025 gestures = me.getGestures(),
60026 gesture;
60027
60028 if (!me.getEnabled()) {
60029 return;
60030 }
60031
60032 function insertGesture(name, fn) {
60033 chart.on(
60034 name,
60035 // wrap the handler so it does not fire if the event is locked by another interaction
60036 me.listeners[name] = function (e) {
60037 var locks = me.getLocks(), result;
60038 if (me.getEnabled() && (!(name in locks) || locks[name] === me)) {
60039 result = (Ext.isFunction(fn) ? fn : me[fn]).apply(this, arguments);
60040 if (result === false && e && e.stopPropagation) {
60041 e.stopPropagation();
60042 }
60043 return result;
60044 }
60045 },
60046 me
60047 );
60048 }
60049
60050 me.listeners = me.listeners || {};
60051 for (gesture in gestures) {
60052 insertGesture(gesture, gestures[gesture]);
60053 }
60054 },
60055
60056 removeChartListener: function (chart) {
60057 var me = this,
60058 gestures = me.getGestures(),
60059 gesture;
60060
60061 function removeGesture(name) {
60062 chart.un(name, me.listeners[name]);
60063 delete me.listeners[name];
60064 }
60065
60066 for (gesture in gestures) {
60067 removeGesture(gesture);
60068 }
60069 },
60070
60071 lockEvents: function () {
60072 var me = this,
60073 locks = me.getLocks(),
60074 args = Array.prototype.slice.call(arguments),
60075 i = args.length;
60076 while (i--) {
60077 locks[args[i]] = me;
60078 }
60079 },
60080
60081 unlockEvents: function () {
60082 var locks = this.getLocks(),
60083 args = Array.prototype.slice.call(arguments),
60084 i = args.length;
60085 while (i--) {
60086 delete locks[args[i]];
60087 }
60088 },
60089
60090 getLocks: function () {
60091 var chart = this.getChart();
60092 return chart.lockedEvents || (chart.lockedEvents = {});
60093 },
60094
60095 isMultiTouch: function () {
60096 if (Ext.browser.is.IE10) {
60097 return true;
60098 }
60099 return !(Ext.os.is.MultiTouch === false || Ext.browser.is.AndroidStock2 || Ext.os.is.Desktop);
60100 },
60101
60102 initializeDefaults: Ext.emptyFn,
60103
60104 doSync: function () {
60105 var chart = this.getChart();
60106 if (this.syncTimer) {
60107 clearTimeout(this.syncTimer);
60108 this.syncTimer = null;
60109 }
60110 if (this.stopAnimationBeforeSync) {
60111 chart.resizing = true;
60112 }
60113 chart.redraw();
60114 if (this.stopAnimationBeforeSync) {
60115 chart.resizing = false;
60116 }
60117 this.syncThrottle = Date.now() + this.throttleGap;
60118 },
60119
60120 sync: function () {
60121 var me = this;
60122 if (me.throttleGap && Ext.frameStartTime < me.syncThrottle) {
60123 if (me.syncTimer) {
60124 return;
60125 }
60126 me.syncTimer = setTimeout(function () {
60127 me.doSync();
60128 }, me.throttleGap);
60129 } else {
60130 me.doSync();
60131 }
60132 },
60133
60134 getItemId: function () {
60135 return this.getId();
60136 },
60137
60138 isXType: function (xtype) {
60139 return xtype === 'interaction';
60140 },
60141
60142 destroy: function () {
60143 Ext.ComponentManager.unregister(this);
60144 this.listeners = [];
60145 this.callSuper();
60146 }
60147 }, function () {
60148 if (Ext.browser.is.AndroidStock2) {
60149 this.prototype.throttleGap = 20;
60150 } else if (Ext.os.is.Android4) {
60151 this.prototype.throttleGap = 40;
60152 }
60153 });
60154
60155 /**
60156 * @class Ext.chart.MarkerHolder
60157 * @extends Ext.mixin.Mixin
60158 *
60159 * Mixin that provides the functionality to place markers.
60160 */
60161 Ext.define('Ext.chart.MarkerHolder', {
60162 extend: Ext.mixin.Mixin ,
60163 mixinConfig: {
60164 id: 'markerHolder',
60165 hooks: {
60166 constructor: 'constructor',
60167 preRender: 'preRender'
60168 }
60169 },
60170
60171 isMarkerHolder: true,
60172
60173 constructor: function () {
60174 this.boundMarkers = {};
60175 this.cleanRedraw = false;
60176 },
60177
60178 /**
60179 *
60180 * @param {String} name
60181 * @param {Ext.chart.Markers} marker
60182 */
60183 bindMarker: function (name, marker) {
60184 if (marker) {
60185 if (!this.boundMarkers[name]) {
60186 this.boundMarkers[name] = [];
60187 }
60188 Ext.Array.include(this.boundMarkers[name], marker);
60189 }
60190 },
60191
60192 getBoundMarker: function (name) {
60193 return this.boundMarkers[name];
60194 },
60195
60196 preRender: function () {
60197 var boundMarkers = this.boundMarkers, boundMarkersItem,
60198 name, i, ln, id = this.getId(),
60199 parent = this.getParent(),
60200 matrix = this.surfaceMatrix ? this.surfaceMatrix.set(1, 0, 0, 1, 0, 0) : (this.surfaceMatrix = new Ext.draw.Matrix());
60201
60202 this.cleanRedraw = !this.attr.dirty;
60203 if (!this.cleanRedraw) {
60204 for (name in this.boundMarkers) {
60205 if (boundMarkers[name]) {
60206 for (boundMarkersItem = boundMarkers[name], i = 0, ln = boundMarkersItem.length; i < ln; i++) {
60207 boundMarkersItem[i].clear(id);
60208 }
60209 }
60210 }
60211 }
60212
60213 while (parent && parent.attr && parent.attr.matrix) {
60214 matrix.prependMatrix(parent.attr.matrix);
60215 parent = parent.getParent();
60216 }
60217 matrix.prependMatrix(parent.matrix);
60218 this.surfaceMatrix = matrix;
60219 this.inverseSurfaceMatrix = matrix.inverse(this.inverseSurfaceMatrix);
60220 },
60221
60222 putMarker: function (name, markerAttr, index, canonical, keepRevision) {
60223 var boundMarkersItem, i, ln, id = this.getId();
60224 if (this.boundMarkers[name]) {
60225 for (boundMarkersItem = this.boundMarkers[name], i = 0, ln = boundMarkersItem.length; i < ln; i++) {
60226 boundMarkersItem[i].putMarkerFor(id, markerAttr, index, canonical);
60227 }
60228 }
60229 },
60230
60231 getMarkerBBox: function (name, index, isWithoutTransform) {
60232 var id = this.getId(),
60233 left = Infinity,
60234 right = -Infinity,
60235 top = Infinity,
60236 bottom = -Infinity,
60237 bbox, boundMarker, i, ln;
60238
60239 if (this.boundMarkers[name]) {
60240 for (boundMarker = this.boundMarkers[name], i = 0, ln = boundMarker.length; i < ln; i++) {
60241 bbox = boundMarker[i].getMarkerBBoxFor(id, index, isWithoutTransform);
60242 if (left > bbox.x) {
60243 left = bbox.x;
60244 }
60245 if (right < bbox.x + bbox.width) {
60246 right = bbox.x + bbox.width;
60247 }
60248 if (top > bbox.y) {
60249 top = bbox.y;
60250 }
60251 if (bottom < bbox.y + bbox.height) {
60252 bottom = bbox.y + bbox.height;
60253 }
60254 }
60255 }
60256 return {
60257 x: left,
60258 y: top,
60259 width: right - left,
60260 height: bottom - top
60261 };
60262 }
60263 });
60264
60265 /**
60266 * @private
60267 * @class Ext.chart.axis.sprite.Axis
60268 * @extends Ext.draw.sprite.Sprite
60269 *
60270 * The axis sprite. Currently all types of the axis will be rendered with this sprite.
60271 * TODO(touch-2.2): Split different types of axis into different sprite classes.
60272 */
60273 Ext.define('Ext.chart.axis.sprite.Axis', {
60274 extend: Ext.draw.sprite.Sprite ,
60275 mixins: {
60276 markerHolder: Ext.chart.MarkerHolder
60277 },
60278
60279
60280
60281 inheritableStatics: {
60282 def: {
60283 processors: {
60284 /**
60285 * @cfg {Boolean} grid 'true' if the axis has a grid.
60286 */
60287 grid: 'bool',
60288
60289 /**
60290 * @cfg {Boolean} axisLine 'true' if the main line of the axis is drawn.
60291 */
60292 axisLine: 'bool',
60293
60294 /**
60295 * @cfg {Boolean} minorTricks 'true' if the axis has sub ticks.
60296 */
60297 minorTicks: 'bool',
60298
60299 /**
60300 * @cfg {Number} minorTickSize The length of the minor ticks.
60301 */
60302 minorTickSize: 'number',
60303
60304 /**
60305 * @cfg {Boolean} majorTicks 'true' if the axis has major ticks.
60306 */
60307 majorTicks: 'bool',
60308
60309 /**
60310 * @cfg {Number} majorTickSize The length of the major ticks.
60311 */
60312 majorTickSize: 'number',
60313
60314 /**
60315 * @cfg {Number} length The total length of the axis.
60316 */
60317 length: 'number',
60318
60319 /**
60320 * @private
60321 * @cfg {Number} startGap Axis start determined by the chart inset padding.
60322 */
60323 startGap: 'number',
60324
60325 /**
60326 * @private
60327 * @cfg {Number} endGap Axis end determined by the chart inset padding.
60328 */
60329 endGap: 'number',
60330
60331 /**
60332 * @cfg {Number} dataMin The minimum value of the axis data.
60333 */
60334 dataMin: 'number',
60335
60336 /**
60337 * @cfg {Number} dataMax The maximum value of the axis data.
60338 */
60339 dataMax: 'number',
60340
60341 /**
60342 * @cfg {Number} visibleMin The minimum value that is displayed.
60343 */
60344 visibleMin: 'number',
60345
60346 /**
60347 * @cfg {Number} visibleMax The maximum value that is displayed.
60348 */
60349 visibleMax: 'number',
60350
60351 /**
60352 * @cfg {String} position The position of the axis on the chart.
60353 */
60354 position: 'enums(left,right,top,bottom,angular,radial)',
60355
60356 /**
60357 * @cfg {Number} minStepSize The minimum step size between ticks.
60358 */
60359 minStepSize: 'number',
60360
60361 /**
60362 * @private
60363 * @cfg {Number} estStepSize The estimated step size between ticks.
60364 */
60365 estStepSize: 'number',
60366
60367 /**
60368 * @private
60369 * Unused.
60370 */
60371 titleOffset: 'number',
60372
60373 /**
60374 * @cfg {Number} textPadding The padding around axis labels to determine collision.
60375 */
60376 textPadding: 'number',
60377
60378 /**
60379 * @cfg {Number} min The minimum value of the axis.
60380 */
60381 min: 'number',
60382
60383 /**
60384 * @cfg {Number} max The maximum value of the axis.
60385 */
60386 max: 'number',
60387
60388 /**
60389 * @cfg {Number} centerX The central point of the angular axis on the x-axis.
60390 */
60391 centerX: 'number',
60392
60393 /**
60394 * @cfg {Number} centerY The central point of the angular axis on the y-axis.
60395 */
60396 centerY: 'number',
60397
60398 /**
60399 * @private
60400 * @cfg {Number} radius
60401 * Unused.
60402 */
60403 radius: 'number',
60404
60405 /**
60406 * @cfg {Number} The starting rotation of the angular axis.
60407 */
60408 baseRotation: 'number',
60409
60410 /**
60411 * @private
60412 * Unused.
60413 */
60414 data: 'default',
60415
60416 /**
60417 * @cfg {Boolean} 'true' if the estimated step size is adjusted by text size.
60418 */
60419 enlargeEstStepSizeByText: 'bool'
60420 },
60421
60422 defaults: {
60423 grid: false,
60424 axisLine: true,
60425 minorTicks: false,
60426 minorTickSize: 3,
60427 majorTicks: true,
60428 majorTickSize: 5,
60429 length: 0,
60430 startGap: 0,
60431 endGap: 0,
60432 visibleMin: 0,
60433 visibleMax: 1,
60434 dataMin: 0,
60435 dataMax: 1,
60436 position: '',
60437 minStepSize: 0,
60438 estStepSize: 20,
60439 min: 0,
60440 max: 1,
60441 centerX: 0,
60442 centerY: 0,
60443 radius: 1,
60444 baseRotation: 0,
60445 data: null,
60446 titleOffset: 0,
60447 textPadding: 5,
60448 scalingCenterY: 0,
60449 scalingCenterX: 0,
60450 // Override default
60451 strokeStyle: 'black',
60452 enlargeEstStepSizeByText: false
60453 },
60454
60455 dirtyTriggers: {
60456 minorTickSize: 'bbox',
60457 majorTickSize: 'bbox',
60458 position: 'bbox,layout',
60459 axisLine: 'bbox,layout',
60460 min: 'layout',
60461 max: 'layout',
60462 length: 'layout',
60463 minStepSize: 'layout',
60464 estStepSize: 'layout',
60465 data: 'layout',
60466 dataMin: 'layout',
60467 dataMax: 'layout',
60468 visibleMin: 'layout',
60469 visibleMax: 'layout',
60470 enlargeEstStepSizeByText: 'layout'
60471 },
60472 updaters: {
60473 'layout': function () {
60474 this.doLayout();
60475 }
60476 }
60477 }
60478 },
60479
60480 config: {
60481
60482 /**
60483 * @cfg {Object} label
60484 *
60485 * The label configuration object for the Axis. This object may include style attributes
60486 * like `spacing`, `padding`, `font` that receives a string or number and
60487 * returns a new string with the modified values.
60488 */
60489 label: null,
60490
60491 /**
60492 * @cfg {Object|Ext.chart.axis.layout.Layout} layout The layout configuration used by the axis.
60493 */
60494 layout: null,
60495
60496 /**
60497 * @cfg {Object|Ext.chart.axis.segmenter.Segmenter} segmenter The method of segmenter used by the axis.
60498 */
60499 segmenter: null,
60500
60501 /**
60502 * @cfg {Function} renderer Allows direct customisation of rendered axis sprites.
60503 */
60504 renderer: null,
60505
60506 /**
60507 * @private
60508 * @cfg {Object} layoutContext Stores the context after calculating layout.
60509 */
60510 layoutContext: null,
60511
60512 /**
60513 * @cfg {Ext.chart.axis.Axis} axis The axis represented by the this sprite.
60514 */
60515 axis: null
60516 },
60517
60518 thickness: 0,
60519
60520 stepSize: 0,
60521
60522 getBBox: function () { return null; },
60523
60524 doLayout: function () {
60525 var me = this,
60526 attr = me.attr,
60527 layout = me.getLayout(),
60528 min = attr.dataMin + (attr.dataMax - attr.dataMin) * attr.visibleMin,
60529 max = attr.dataMin + (attr.dataMax - attr.dataMin) * attr.visibleMax,
60530 context = {
60531 attr: attr,
60532 segmenter: me.getSegmenter()
60533 };
60534
60535 if (attr.position === 'left' || attr.position === 'right') {
60536 attr.translationX = 0;
60537 attr.translationY = max * attr.length / (max - min);
60538 attr.scalingX = 1;
60539 attr.scalingY = -attr.length / (max - min);
60540 attr.scalingCenterY = 0;
60541 attr.scalingCenterX = 0;
60542 me.applyTransformations(true);
60543 } else if (attr.position === 'top' || attr.position === 'bottom') {
60544 attr.translationX = -min * attr.length / (max - min);
60545 attr.translationY = 0;
60546 attr.scalingX = attr.length / (max - min);
60547 attr.scalingY = 1;
60548 attr.scalingCenterY = 0;
60549 attr.scalingCenterX = 0;
60550 me.applyTransformations(true);
60551 }
60552
60553 if (layout) {
60554 layout.calculateLayout(context);
60555 me.setLayoutContext(context);
60556 }
60557 },
60558
60559 iterate: function (snaps, fn) {
60560 var i, position;
60561 if (snaps.getLabel) {
60562 if (snaps.min < snaps.from) {
60563 fn.call(this, snaps.min, snaps.getLabel(snaps.min), -1, snaps);
60564 }
60565 for (i = 0; i <= snaps.steps; i++) {
60566 fn.call(this, snaps.get(i), snaps.getLabel(i), i, snaps);
60567 }
60568 if (snaps.max > snaps.to) {
60569 fn.call(this, snaps.max, snaps.getLabel(snaps.max), snaps.steps + 1, snaps);
60570 }
60571 } else {
60572 if (snaps.min < snaps.from) {
60573 fn.call(this, snaps.min, snaps.min, -1, snaps);
60574 }
60575 for (i = 0; i <= snaps.steps; i++) {
60576 position = snaps.get(i);
60577 fn.call(this, position, position, i, snaps);
60578 }
60579 if (snaps.max > snaps.to) {
60580 fn.call(this, snaps.max, snaps.max, snaps.steps + 1, snaps);
60581 }
60582 }
60583 },
60584
60585 renderTicks: function (surface, ctx, layout, clipRegion) {
60586 var me = this,
60587 attr = me.attr,
60588 docked = attr.position,
60589 matrix = attr.matrix,
60590 halfLineWidth = 0.5 * attr.lineWidth,
60591 xx = matrix.getXX(),
60592 dx = matrix.getDX(),
60593 yy = matrix.getYY(),
60594 dy = matrix.getDY(),
60595 majorTicks = layout.majorTicks,
60596 majorTickSize = attr.majorTickSize,
60597 minorTicks = layout.minorTicks,
60598 minorTickSize = attr.minorTickSize;
60599
60600 if (majorTicks) {
60601 switch (docked) {
60602 case 'right':
60603 function getRightTickFn(size) {
60604 return function (position, labelText, i) {
60605 position = surface.roundPixel(position * yy + dy) + halfLineWidth;
60606 ctx.moveTo(0, position);
60607 ctx.lineTo(size, position);
60608 };
60609 }
60610 me.iterate(majorTicks, getRightTickFn(majorTickSize));
60611 minorTicks && me.iterate(minorTicks, getRightTickFn(minorTickSize));
60612 break;
60613 case 'left':
60614 function getLeftTickFn(size) {
60615 return function (position, labelText, i) {
60616 position = surface.roundPixel(position * yy + dy) + halfLineWidth;
60617 ctx.moveTo(clipRegion[2] - size, position);
60618 ctx.lineTo(clipRegion[2], position);
60619 };
60620 }
60621 me.iterate(majorTicks, getLeftTickFn(majorTickSize));
60622 minorTicks && me.iterate(minorTicks, getLeftTickFn(minorTickSize));
60623 break;
60624 case 'bottom':
60625 function getBottomTickFn(size) {
60626 return function (position, labelText, i) {
60627 position = surface.roundPixel(position * xx + dx) - halfLineWidth;
60628 ctx.moveTo(position, 0);
60629 ctx.lineTo(position, size);
60630 };
60631 }
60632 me.iterate(majorTicks, getBottomTickFn(majorTickSize));
60633 minorTicks && me.iterate(minorTicks, getBottomTickFn(minorTickSize));
60634 break;
60635 case 'top':
60636 function getTopTickFn(size) {
60637 return function (position, labelText, i) {
60638 position = surface.roundPixel(position * xx + dx) - halfLineWidth;
60639 ctx.moveTo(position, clipRegion[3]);
60640 ctx.lineTo(position, clipRegion[3] - size);
60641 };
60642 }
60643 me.iterate(majorTicks, getTopTickFn(majorTickSize));
60644 minorTicks && me.iterate(minorTicks, getTopTickFn(minorTickSize));
60645 break;
60646 case 'angular':
60647 me.iterate(majorTicks, function (position, labelText, i) {
60648 position = position / (attr.max + 1) * Math.PI * 2 + attr.baseRotation;
60649 ctx.moveTo(
60650 attr.centerX + (attr.length) * Math.cos(position),
60651 attr.centerY + (attr.length) * Math.sin(position)
60652 );
60653 ctx.lineTo(
60654 attr.centerX + (attr.length + majorTickSize) * Math.cos(position),
60655 attr.centerY + (attr.length + majorTickSize) * Math.sin(position)
60656 );
60657 });
60658 break;
60659 }
60660 }
60661 },
60662
60663 renderLabels: function (surface, ctx, layout, clipRegion) {
60664 var me = this,
60665 attr = me.attr,
60666 halfLineWidth = 0.5 * attr.lineWidth,
60667 docked = attr.position,
60668 matrix = attr.matrix,
60669 textPadding = attr.textPadding,
60670 xx = matrix.getXX(),
60671 dx = matrix.getDX(),
60672 yy = matrix.getYY(),
60673 dy = matrix.getDY(),
60674 thickness = 0,
60675 majorTicks = layout.majorTicks,
60676 padding = Math.max(attr.majorTickSize, attr.minorTickSize) + attr.lineWidth,
60677 label = this.getLabel(), font,
60678 lastLabelText = null,
60679 textSize = 0, textCount = 0,
60680 segmenter = layout.segmenter,
60681 renderer = this.getRenderer(),
60682 labelInverseMatrix, lastBBox = null, bbox, fly, text;
60683 if (majorTicks && label && !label.attr.hidden) {
60684 font = label.attr.font;
60685 if (ctx.font !== font) {
60686 ctx.font = font;
60687 } // This can profoundly improve performance.
60688 label.setAttributes({translationX: 0, translationY: 0}, true, true);
60689 label.applyTransformations();
60690 labelInverseMatrix = label.attr.inverseMatrix.elements.slice(0);
60691 switch (docked) {
60692 case 'left':
60693 label.setAttributes({
60694 translationX: surface.roundPixel(clipRegion[2] - padding + dx) - halfLineWidth - me.thickness / 2
60695 }, true, true);
60696 break;
60697 case 'right':
60698 label.setAttributes({
60699 translationX: surface.roundPixel(padding + dx) - halfLineWidth + me.thickness / 2
60700 }, true, true);
60701 break;
60702 case 'top':
60703 label.setAttributes({
60704 translationY: surface.roundPixel(clipRegion[3] - padding) - halfLineWidth - me.thickness / 2
60705 }, true, true);
60706 break;
60707 case 'bottom':
60708 label.setAttributes({
60709 translationY: surface.roundPixel(padding) - halfLineWidth + me.thickness / 2
60710 }, true, true);
60711 break;
60712 case 'radial' :
60713 label.setAttributes({
60714 translationX: attr.centerX
60715 }, true, true);
60716 break;
60717 case 'angular':
60718 label.setAttributes({
60719 translationY: attr.centerY
60720 }, true, true);
60721 break;
60722 }
60723
60724 // TODO: there are better ways to detect collision.
60725 if (docked === 'left' || docked === 'right') {
60726 me.iterate(majorTicks, function (position, labelText, i) {
60727 if (labelText === undefined) {
60728 return;
60729 }
60730 text = renderer ? renderer.call(this, labelText, layout, lastLabelText) : segmenter.renderer(labelText, layout, lastLabelText);
60731 lastLabelText = labelText;
60732 label.setAttributes({
60733 text: String(text),
60734 translationY: surface.roundPixel(position * yy + dy)
60735 }, true, true);
60736 label.applyTransformations();
60737 thickness = Math.max(thickness, label.getBBox().width + padding);
60738 if (thickness <= me.thickness) {
60739 fly = Ext.draw.Matrix.fly(label.attr.matrix.elements.slice(0));
60740 bbox = fly.prepend.apply(fly, labelInverseMatrix).transformBBox(label.getBBox(true));
60741 if (lastBBox && !Ext.draw.Draw.isBBoxIntersect(bbox, lastBBox, textPadding)) {
60742 return;
60743 }
60744 surface.renderSprite(label);
60745 lastBBox = bbox;
60746 textSize += bbox.height;
60747 textCount++;
60748 }
60749 });
60750 } else if (docked === 'top' || docked === 'bottom') {
60751 me.iterate(majorTicks, function (position, labelText, i) {
60752 if (labelText === undefined) {
60753 return;
60754 }
60755 text = renderer ? renderer.call(this, labelText, layout, lastLabelText) : segmenter.renderer(labelText, layout, lastLabelText);
60756 lastLabelText = labelText;
60757 label.setAttributes({
60758 text: String(text),
60759 translationX: surface.roundPixel(position * xx + dx)
60760 }, true, true);
60761 label.applyTransformations();
60762 thickness = Math.max(thickness, label.getBBox().height + padding);
60763 if (thickness <= me.thickness) {
60764 fly = Ext.draw.Matrix.fly(label.attr.matrix.elements.slice(0));
60765 bbox = fly.prepend.apply(fly, labelInverseMatrix).transformBBox(label.getBBox(true));
60766 if (lastBBox && !Ext.draw.Draw.isBBoxIntersect(bbox, lastBBox, textPadding)) {
60767 return;
60768 }
60769 surface.renderSprite(label);
60770 lastBBox = bbox;
60771 textSize += bbox.width;
60772 textCount++;
60773 }
60774 });
60775 } else if (docked === 'radial') {
60776 me.iterate(majorTicks, function (position, labelText, i) {
60777 if (labelText === undefined) {
60778 return;
60779 }
60780 text = renderer ? renderer.call(this, labelText, layout, lastLabelText) : segmenter.renderer(labelText, layout, lastLabelText);
60781 lastLabelText = labelText;
60782 if (typeof text !== 'undefined') {
60783 label.setAttributes({
60784 text: String(text),
60785 translationY: attr.centerY - surface.roundPixel(position) / attr.max * attr.length
60786 }, true, true);
60787 label.applyTransformations();
60788 bbox = label.attr.matrix.transformBBox(label.getBBox(true));
60789 if (lastBBox && !Ext.draw.Draw.isBBoxIntersect(bbox, lastBBox)) {
60790 return;
60791 }
60792 surface.renderSprite(label);
60793 lastBBox = bbox;
60794 textSize += bbox.width;
60795 textCount++;
60796 }
60797 });
60798 } else if (docked === 'angular') {
60799 me.iterate(majorTicks, function (position, labelText, i) {
60800 if (labelText === undefined) {
60801 return;
60802 }
60803 text = renderer ? renderer.call(this, labelText, layout, lastLabelText) : segmenter.renderer(labelText, layout, lastLabelText);
60804 lastLabelText = labelText;
60805
60806 if (typeof text !== 'undefined') {
60807 var angle = position / (attr.max + 1) * Math.PI * 2 + attr.baseRotation;
60808 label.setAttributes({
60809 text: String(text),
60810 translationX: attr.centerX + (attr.length + 10) * Math.cos(angle),
60811 translationY: attr.centerY + (attr.length + 10) * Math.sin(angle)
60812 }, true, true);
60813 label.applyTransformations();
60814 bbox = label.attr.matrix.transformBBox(label.getBBox(true));
60815 if (lastBBox && !Ext.draw.Draw.isBBoxIntersect(bbox, lastBBox)) {
60816 return;
60817 }
60818 surface.renderSprite(label);
60819 lastBBox = bbox;
60820 textSize += bbox.width;
60821 textCount++;
60822 }
60823 });
60824 }
60825
60826 if (attr.enlargeEstStepSizeByText && textCount) {
60827 textSize /= textCount;
60828 textSize += padding;
60829 textSize *= 2;
60830 if (attr.estStepSize < textSize) {
60831 attr.estStepSize = textSize;
60832 }
60833 }
60834
60835 if (Math.abs(me.thickness - (thickness)) > 1) {
60836 me.thickness = thickness;
60837 attr.bbox.plain.dirty = true;
60838 attr.bbox.transform.dirty = true;
60839 me.doThicknessChanged();
60840 return false;
60841 }
60842 }
60843 },
60844
60845 renderAxisLine: function (surface, ctx, layout, clipRegion) {
60846 var me = this,
60847 attr = me.attr,
60848 halfLineWidth = attr.lineWidth * 0.5,
60849 docked = attr.position,
60850 position;
60851 if (attr.axisLine) {
60852 switch (docked) {
60853 case 'left':
60854 position = surface.roundPixel(clipRegion[2]) - halfLineWidth;
60855 ctx.moveTo(position, -attr.endGap);
60856 ctx.lineTo(position, attr.length + attr.startGap);
60857 break;
60858 case 'right':
60859 ctx.moveTo(halfLineWidth, -attr.endGap);
60860 ctx.lineTo(halfLineWidth, attr.length + attr.startGap);
60861 break;
60862 case 'bottom':
60863 ctx.moveTo(-attr.startGap, halfLineWidth);
60864 ctx.lineTo(attr.length + attr.endGap, halfLineWidth);
60865 break;
60866 case 'top':
60867 position = surface.roundPixel(clipRegion[3]) - halfLineWidth;
60868 ctx.moveTo(-attr.startGap, position);
60869 ctx.lineTo(attr.length + attr.endGap, position);
60870 break;
60871 case 'angular':
60872 ctx.moveTo(attr.centerX + attr.length, attr.centerY);
60873 ctx.arc(attr.centerX, attr.centerY, attr.length, 0, Math.PI * 2, true);
60874 break;
60875 }
60876 }
60877 },
60878
60879 renderGridLines: function (surface, ctx, layout, clipRegion) {
60880 var me = this,
60881 attr = me.attr,
60882 matrix = attr.matrix,
60883 startGap = attr.startGap,
60884 endGap = attr.endGap,
60885 xx = matrix.getXX(),
60886 yy = matrix.getYY(),
60887 dx = matrix.getDX(),
60888 dy = matrix.getDY(),
60889 position = attr.position,
60890 majorTicks = layout.majorTicks,
60891 anchor, j, lastAnchor;
60892 if (attr.grid) {
60893 if (majorTicks) {
60894 if (position === 'left' || position === 'right') {
60895 lastAnchor = attr.min * yy + dy + endGap + startGap;
60896 me.iterate(majorTicks, function (position, labelText, i) {
60897 anchor = position * yy + dy + endGap;
60898 me.putMarker('horizontal-' + (i % 2 ? 'odd' : 'even'), {
60899 y: anchor,
60900 height: lastAnchor - anchor
60901 }, j = i, true);
60902 lastAnchor = anchor;
60903 });
60904 j++;
60905 anchor = 0;
60906 me.putMarker('horizontal-' + (j % 2 ? 'odd' : 'even'), {
60907 y: anchor,
60908 height: lastAnchor - anchor
60909 }, j, true);
60910 } else if (position === 'top' || position === 'bottom') {
60911 lastAnchor = attr.min * xx + dx + startGap;
60912 if (startGap) {
60913 me.putMarker('vertical-even', {
60914 x: 0,
60915 width: lastAnchor
60916 }, -1, true);
60917 }
60918 me.iterate(majorTicks, function (position, labelText, i) {
60919 anchor = position * xx + dx + startGap;
60920 me.putMarker('vertical-' + (i % 2 ? 'odd' : 'even'), {
60921 x: anchor,
60922 width: lastAnchor - anchor
60923 }, j = i, true);
60924 lastAnchor = anchor;
60925 });
60926 j++;
60927 anchor = attr.length + attr.startGap + attr.endGap;
60928 me.putMarker('vertical-' + (j % 2 ? 'odd' : 'even'), {
60929 x: anchor,
60930 width: lastAnchor - anchor
60931 }, j, true);
60932 } else if (position === 'radial') {
60933 me.iterate(majorTicks, function (position, labelText, i) {
60934 anchor = position / attr.max * attr.length;
60935 me.putMarker('circular-' + (i % 2 ? 'odd' : 'even'), {
60936 scalingX: anchor,
60937 scalingY: anchor
60938 }, i, true);
60939 lastAnchor = anchor;
60940 });
60941 } else if (position === 'angular') {
60942 me.iterate(majorTicks, function (position, labelText, i) {
60943 anchor = position / (attr.max + 1) * Math.PI * 2 + attr.baseRotation;
60944 me.putMarker('radial-' + (i % 2 ? 'odd' : 'even'), {
60945 rotationRads: anchor,
60946 rotationCenterX: 0,
60947 rotationCenterY: 0,
60948 scalingX: attr.length,
60949 scalingY: attr.length
60950 }, i, true);
60951 lastAnchor = anchor;
60952 });
60953 }
60954 }
60955 }
60956 },
60957
60958 doThicknessChanged: function () {
60959 var axis = this.getAxis();
60960 if (axis) {
60961 axis.onThicknessChanged();
60962 }
60963 },
60964
60965 render: function (surface, ctx, clipRegion) {
60966 var me = this,
60967 layout = me.getLayoutContext();
60968 if (layout) {
60969 if (false === me.renderLabels(surface, ctx, layout, clipRegion)) {
60970 return false;
60971 }
60972 ctx.beginPath();
60973 me.renderTicks(surface, ctx, layout, clipRegion);
60974 me.renderAxisLine(surface, ctx, layout, clipRegion);
60975 me.renderGridLines(surface, ctx, layout, clipRegion);
60976 ctx.stroke();
60977 }
60978 }
60979 });
60980
60981 /**
60982 * @abstract
60983 * @class Ext.chart.axis.segmenter.Segmenter
60984 *
60985 * Interface for a segmenter in an Axis. A segmenter defines the operations you can do to a specific
60986 * data type.
60987 *
60988 * See {@link Ext.chart.axis.Axis}.
60989 *
60990 */
60991 Ext.define('Ext.chart.axis.segmenter.Segmenter', {
60992
60993 config: {
60994 /**
60995 * @cfg {Ext.chart.axis.Axis} axis The axis that the Segmenter is bound.
60996 */
60997 axis: null
60998 },
60999
61000 constructor: function (config) {
61001 this.initConfig(config);
61002 },
61003
61004 /**
61005 * This method formats the value.
61006 *
61007 * @param {*} value The value to format.
61008 * @param {Object} context Axis layout context.
61009 * @return {String}
61010 */
61011 renderer: function (value, context) {
61012 return String(value);
61013 },
61014
61015 /**
61016 * Convert from any data into the target type.
61017 * @param {*} value The value to convert from
61018 * @return {*} The converted value.
61019 */
61020 from: function (value) {
61021 return value;
61022 },
61023
61024 /**
61025 * Returns the difference between the min and max value based on the given unit scale.
61026 *
61027 * @param {*} min The smaller value.
61028 * @param {*} max The larger value.
61029 * @param {*} unit The unit scale. Unit can be any type.
61030 * @return {Number} The number of `unit`s between min and max. It is the minimum n that min + n * unit >= max.
61031 */
61032 diff: Ext.emptyFn,
61033
61034 /**
61035 * Align value with step of units.
61036 * For example, for the date segmenter, if the unit is "Month" and step is 3, the value will be aligned by
61037 * seasons.
61038 *
61039 * @param {*} value The value to be aligned.
61040 * @param {Number} step The step of units.
61041 * @param {*} unit The unit.
61042 * @return {*} Aligned value.
61043 */
61044 align: Ext.emptyFn,
61045
61046 /**
61047 * Add `step` `unit`s to the value.
61048 * @param {*} value The value to be added.
61049 * @param {Number} step The step of units. Negative value are allowed.
61050 * @param {*} unit The unit.
61051 */
61052 add: Ext.emptyFn,
61053
61054 /**
61055 * Given a start point and estimated step size of a range, determine the preferred step size.
61056 *
61057 * @param {*} start The start point of range.
61058 * @param {*} estStepSize The estimated step size.
61059 * @return {Object} Return the step size by an object of step x unit.
61060 * @return {Number} return.step The step count of units.
61061 * @return {Number|Object} return.unit The unit.
61062 */
61063 preferredStep: Ext.emptyFn
61064 });
61065
61066 /**
61067 * @class Ext.chart.axis.segmenter.Names
61068 * @extends Ext.chart.axis.segmenter.Segmenter
61069 *
61070 * Names data type. Names will be calculated as their indices in the methods in this class.
61071 * The `preferredStep` always return `{ unit: 1, step: 1 }` to indicate "show every item".
61072 *
61073 */
61074 Ext.define("Ext.chart.axis.segmenter.Names", {
61075 extend: Ext.chart.axis.segmenter.Segmenter ,
61076 alias: 'segmenter.names',
61077
61078 renderer: function (value, context) {
61079 return value;
61080 },
61081
61082 diff: function (min, max, unit) {
61083 return Math.floor(max - min);
61084 },
61085
61086 align: function (value, step, unit) {
61087 return Math.floor(value);
61088 },
61089
61090
61091 add: function (value, step, unit) {
61092 return value + step;
61093 },
61094
61095 preferredStep: function (min, estStepSize, minIdx, data) {
61096 return {
61097 unit: 1,
61098 step: 1
61099 };
61100 }
61101 });
61102
61103 /**
61104 * @class Ext.chart.axis.segmenter.Time
61105 * @extends Ext.chart.axis.segmenter.Segmenter
61106 *
61107 * Time data type.
61108 */
61109 Ext.define('Ext.chart.axis.segmenter.Time', {
61110 extend: Ext.chart.axis.segmenter.Segmenter ,
61111 alias: 'segmenter.time',
61112
61113 config: {
61114 /**
61115 * @cfg {Object} step
61116 * If specified, the will override the result of {@link #preferredStep}.
61117 */
61118 step: null
61119 },
61120
61121 renderer: function (value, context) {
61122 var ExtDate = Ext.Date;
61123 switch (context.majorTicks.unit) {
61124 case 'y':
61125 return ExtDate.format(value, 'Y');
61126 case 'mo':
61127 return ExtDate.format(value, 'Y-m');
61128 case 'd':
61129 return ExtDate.format(value, 'Y-m-d');
61130 }
61131 return ExtDate.format(value, 'Y-m-d\nH:i:s');
61132 },
61133
61134 from: function (value) {
61135 return new Date(value);
61136 },
61137
61138 diff: function (min, max, unit) {
61139 var ExtDate = Ext.Date;
61140 if (isFinite(min)) {
61141 min = new Date(min);
61142 }
61143 if (isFinite(max)) {
61144 max = new Date(max);
61145 }
61146 return ExtDate.diff(min, max, unit);
61147 },
61148
61149 align: function (date, step, unit) {
61150 if (unit === 'd' && step >= 7) {
61151 date = Ext.Date.align(date, 'd', step);
61152 date.setDate(date.getDate() - date.getDay() + 1);
61153 return date;
61154 } else {
61155 return Ext.Date.align(date, unit, step);
61156 }
61157 },
61158
61159 add: function (value, step, unit) {
61160 return Ext.Date.add(new Date(value), unit, step);
61161 },
61162
61163 preferredStep: function (min, estStepSize) {
61164 if (this.getStep()) {
61165 return this.getStep();
61166 }
61167 var from = new Date(+min),
61168 to = new Date(+min + Math.ceil(estStepSize)),
61169 ExtDate = Ext.Date,
61170 units = [
61171 [ExtDate.YEAR, 1, 2, 5, 10, 20, 50, 100, 200, 500],
61172 [ExtDate.MONTH, 1, 3, 6],
61173 [ExtDate.DAY, 1, 7, 14],
61174 [ExtDate.HOUR, 1, 6, 12],
61175 [ExtDate.MINUTE, 1, 5, 15, 30],
61176 [ExtDate.SECOND, 1, 5, 15, 30],
61177 [ExtDate.MILLI, 1, 2, 5, 10, 20, 50, 100, 200, 500]
61178 ],
61179 result;
61180
61181 for (var i = 0; i < units.length; i++) {
61182 var unit = units[i][0],
61183 diff = this.diff(from, to, unit);
61184 if (diff > 0) {
61185 for (var j = 1; j < units[i].length; j++) {
61186 if (diff <= units[i][j]) {
61187 result = {
61188 unit: unit,
61189 step: units[i][j]
61190 };
61191 break;
61192 }
61193 }
61194 if (!result) {
61195 i--;
61196 result = {
61197 unit: units[i][0],
61198 step: 1
61199 };
61200 }
61201 break;
61202 }
61203 }
61204 if (!result) {
61205 result = {unit: ExtDate.DAY, step: 1}; // Default step is one Day.
61206 }
61207 return result;
61208 }
61209 });
61210
61211 /**
61212 * @class Ext.chart.axis.segmenter.Numeric
61213 * @extends Ext.chart.axis.segmenter.Segmenter
61214 *
61215 * Numeric data type.
61216 */
61217 Ext.define('Ext.chart.axis.segmenter.Numeric', {
61218 extend: Ext.chart.axis.segmenter.Segmenter ,
61219 alias: 'segmenter.numeric',
61220
61221 renderer: function (value, context) {
61222 return value.toFixed(Math.max(0, context.majorTicks.unit.fixes));
61223 },
61224
61225 diff: function (min, max, unit) {
61226 return Math.floor((max - min) / unit.scale);
61227 },
61228
61229 align: function (value, step, unit) {
61230 return Math.floor(value / (unit.scale * step)) * unit.scale * step;
61231 },
61232
61233
61234 add: function (value, step, unit) {
61235 return value + step * unit.scale;
61236 },
61237
61238 preferredStep: function (min, estStepSize) {
61239 var logs = Math.floor(Math.log(estStepSize) * Math.LOG10E), // common logarithm of estStepSize
61240 scale = Math.pow(10, logs);
61241 estStepSize /= scale;
61242 if (estStepSize < 2) {
61243 estStepSize = 2;
61244 } else if (estStepSize < 5) {
61245 estStepSize = 5;
61246 } else if (estStepSize < 10) {
61247 estStepSize = 10;
61248 logs++;
61249 }
61250 return {
61251 unit: {
61252 // when estStepSize < 1, rounded down log10(estStepSize) is equal to -number_of_leading_zeros in estStepSize
61253 fixes: -logs, // number of fractional digits
61254 scale: scale
61255 },
61256 step: estStepSize
61257 };
61258 },
61259
61260 /**
61261 * Wraps the provided estimated step size of a range without altering it into a step size object.
61262 *
61263 * @param {*} min The start point of range.
61264 * @param {*} estStepSize The estimated step size.
61265 * @return {Object} Return the step size by an object of step x unit.
61266 * @return {Number} return.step The step count of units.
61267 * @return {Object} return.unit The unit.
61268 */
61269
61270 exactStep: function (min, estStepSize) {
61271 var logs = Math.floor(Math.log(estStepSize) * Math.LOG10E),
61272 scale = Math.pow(10, logs);
61273 return {
61274 unit: {
61275 // add one decimal point if estStepSize is not a multiple of scale
61276 fixes: -logs + (estStepSize % scale === 0 ? 0 : 1),
61277 scale: 1
61278 },
61279 step: estStepSize
61280 }
61281 }
61282 });
61283
61284 /**
61285 * @abstract
61286 * @class Ext.chart.axis.layout.Layout
61287 *
61288 * Interface used by Axis to process its data into a meaningful layout.
61289 */
61290 Ext.define("Ext.chart.axis.layout.Layout", {
61291 config: {
61292 /**
61293 * @cfg {Ext.chart.axis.Axis} axis The axis that the Layout is bound.
61294 */
61295 axis: null
61296 },
61297
61298 constructor: function (config) {
61299 this.initConfig();
61300 },
61301
61302 /**
61303 * Processes the data of the series bound to the axis.
61304 * @param {Ext.chart.series.Series} series The bound series.
61305 */
61306 processData: function (series) {
61307 var me = this,
61308 axis = me.getAxis(),
61309 direction = axis.getDirection(),
61310 boundSeries = axis.boundSeries,
61311 i, ln, item;
61312 if (series) {
61313 series['coordinate' + direction]();
61314 } else {
61315 for (i = 0, ln = boundSeries.length; i < ln; i++) {
61316 item = boundSeries[i];
61317 if (item['get' + direction + 'Axis']() === axis) {
61318 item['coordinate' + direction]();
61319 }
61320 }
61321 }
61322 },
61323
61324 /**
61325 * Calculates the position of major ticks for the axis.
61326 * @param {Object} context
61327 */
61328 calculateMajorTicks: function (context) {
61329 var me = this,
61330 attr = context.attr,
61331 range = attr.max - attr.min,
61332 zoom = range / Math.max(1, attr.length) * (attr.visibleMax - attr.visibleMin),
61333 viewMin = attr.min + range * attr.visibleMin,
61334 viewMax = attr.min + range * attr.visibleMax,
61335 estStepSize = attr.estStepSize * zoom,
61336 out = me.snapEnds(context, attr.min, attr.max, estStepSize);
61337 if (out) {
61338 me.trimByRange(context, out, viewMin, viewMax);
61339 context.majorTicks = out;
61340 }
61341 },
61342
61343 /**
61344 * Calculates the position of sub ticks for the axis.
61345 * @param {Object} context
61346 */
61347 calculateMinorTicks: function (context) {
61348 var attr = context.attr;
61349 if (this.snapMinorEnds) {
61350 context.minorTicks = this.snapMinorEnds(context);
61351 }
61352 },
61353
61354 /**
61355 * Calculates the position of tick marks for the axis.
61356 * @param {Object} context
61357 * @return {*}
61358 */
61359 calculateLayout: function (context) {
61360 var me = this,
61361 attr = context.attr,
61362 majorTicks = attr.majorTicks,
61363 minorTicks = attr.minorTicks;
61364 if (attr.length === 0) {
61365 return null;
61366 }
61367
61368 if (majorTicks) {
61369 this.calculateMajorTicks(context);
61370 if (minorTicks) {
61371 this.calculateMinorTicks(context);
61372 }
61373 }
61374 },
61375
61376 /**
61377 * Snaps the data bound to the axis to meaningful tick marks.
61378 * @param {Object} context
61379 * @param {Number} min
61380 * @param {Number} max
61381 * @param {Number} estStepSize
61382 */
61383 snapEnds: Ext.emptyFn,
61384
61385 /**
61386 * Trims the layout of the axis by the defined minimum and maximum.
61387 * @param {Object} context
61388 * @param {Object} out
61389 * @param {Number} trimMin
61390 * @param {Number} trimMax
61391 */
61392 trimByRange: function (context, out, trimMin, trimMax) {
61393 var segmenter = context.segmenter,
61394 unit = out.unit,
61395 beginIdx = segmenter.diff(out.from, trimMin, unit),
61396 endIdx = segmenter.diff(out.from, trimMax, unit),
61397 begin = Math.max(0, Math.ceil(beginIdx / out.step)),
61398 end = Math.min(out.steps, Math.floor(endIdx / out.step));
61399
61400 if (end < out.steps) {
61401 out.to = segmenter.add(out.from, end * out.step, unit);
61402 }
61403
61404 if (out.max > trimMax) {
61405 out.max = out.to;
61406 }
61407
61408 if (out.from < trimMin) {
61409 out.from = segmenter.add(out.from, begin * out.step, unit);
61410 while (out.from < trimMin) {
61411 begin++;
61412 out.from = segmenter.add(out.from, out.step, unit);
61413 }
61414 }
61415
61416 if (out.min < trimMin) {
61417 out.min = out.from;
61418 }
61419
61420 out.steps = end - begin;
61421 }
61422 });
61423
61424 /**
61425 * @class Ext.chart.axis.layout.Discrete
61426 * @extends Ext.chart.axis.layout.Layout
61427 *
61428 * Simple processor for data that cannot be interpolated.
61429 */
61430 Ext.define('Ext.chart.axis.layout.Discrete', {
61431 extend: Ext.chart.axis.layout.Layout ,
61432 alias: 'axisLayout.discrete',
61433
61434 processData: function () {
61435 var me = this,
61436 axis = me.getAxis(),
61437 boundSeries = axis.boundSeries,
61438 direction = axis.getDirection(),
61439 i, ln, item;
61440 this.labels = [];
61441 this.labelMap = {};
61442 for (i = 0, ln = boundSeries.length; i < ln; i++) {
61443 item = boundSeries[i];
61444 if (item['get' + direction + 'Axis']() === axis) {
61445 item['coordinate' + direction]();
61446 }
61447 }
61448 // About the labels on Category axes (aka. axes with a Discrete layout)...
61449 //
61450 // When the data set from the store changes, series.processData() is called, which does its thing
61451 // at the series level and then calls series.updateLabelData() to update the labels in the sprites
61452 // that belong to the series. At the same time, series.processData() calls axis.processData(), which
61453 // also does its thing but at the axis level, and also needs to update the labels for the sprite(s)
61454 // that belong to the axis. This is not that simple, however. So how are the axis labels rendered?
61455 // First, axis.sprite.Axis.render() calls renderLabels() which obtains the majorTicks from the
61456 // axis.layout and iterate() through them. The majorTicks are an object returned by snapEnds() below
61457 // which provides a getLabel() function that returns the label from the axis.layoutContext.data array.
61458 // So now the question is: how are the labels transferred from the axis.layout to the axis.layoutContext?
61459 // The easy response is: it's in calculateLayout() below. The issue is to call calculateLayout() because
61460 // it takes in an axis.layoutContext that can only be created in axis.sprite.Axis.doLayout(), which is
61461 // a private "updater" function that is called by all the sprite's "dirtyTriggers". Of course, we don't
61462 // want to call doLayout() directly from here, so instead we update the sprite's data attribute, which
61463 // sets the dirtyTrigger which calls doLayout() which calls calculateLayout() etc...
61464 // Note that the sprite's data attribute could be set to any value and it would still result in the
61465 // dirtyTrigger we need. For consistency, however, it is set to the labels.
61466 axis.getSprites()[0].setAttributes({data:this.labels});
61467 },
61468
61469 // @inheritdoc
61470 calculateLayout: function (context) {
61471 context.data = this.labels;
61472 this.callSuper([context]);
61473 },
61474
61475 //@inheritdoc
61476 calculateMajorTicks: function (context) {
61477 var me = this,
61478 attr = context.attr,
61479 data = context.data,
61480 range = attr.max - attr.min,
61481 zoom = range / Math.max(1, attr.length) * (attr.visibleMax - attr.visibleMin),
61482 viewMin = attr.min + range * attr.visibleMin,
61483 viewMax = attr.min + range * attr.visibleMax,
61484 estStepSize = attr.estStepSize * zoom;
61485
61486 var out = me.snapEnds(context, Math.max(0, attr.min), Math.min(attr.max, data.length - 1), estStepSize);
61487 if (out) {
61488 me.trimByRange(context, out, viewMin, viewMax);
61489 context.majorTicks = out;
61490 }
61491 },
61492
61493 // @inheritdoc
61494 snapEnds: function (context, min, max, estStepSize) {
61495 estStepSize = Math.ceil(estStepSize);
61496 var steps = Math.floor((max - min) / estStepSize),
61497 data = context.data;
61498 return {
61499 min: min,
61500 max: max,
61501 from: min,
61502 to: steps * estStepSize + min,
61503 step: estStepSize,
61504 steps: steps,
61505 unit: 1,
61506 getLabel: function (current) {
61507 return data[this.from + this.step * current];
61508 },
61509 get: function (current) {
61510 return this.from + this.step * current;
61511 }
61512 };
61513 },
61514
61515 // @inheritdoc
61516 trimByRange: function (context, out, trimMin, trimMax) {
61517 var unit = out.unit,
61518 beginIdx = Math.ceil((trimMin - out.from) / unit) * unit,
61519 endIdx = Math.floor((trimMax - out.from) / unit) * unit,
61520 begin = Math.max(0, Math.ceil(beginIdx / out.step)),
61521 end = Math.min(out.steps, Math.floor(endIdx / out.step));
61522
61523 if (end < out.steps) {
61524 out.to = end;
61525 }
61526
61527 if (out.max > trimMax) {
61528 out.max = out.to;
61529 }
61530
61531 if (out.from < trimMin && out.step > 0) {
61532 out.from = out.from + begin * out.step * unit;
61533 while (out.from < trimMin) {
61534 begin++;
61535 out.from += out.step * unit;
61536 }
61537 }
61538
61539 if (out.min < trimMin) {
61540 out.min = out.from;
61541 }
61542
61543 out.steps = end - begin;
61544 },
61545
61546 getCoordFor: function (value, field, idx, items) {
61547 this.labels.push(value);
61548 return this.labels.length - 1;
61549 }
61550 });
61551
61552 /**
61553 * @class Ext.chart.axis.layout.Continuous
61554 * @extends Ext.chart.axis.layout.Layout
61555 *
61556 * Processor for axis data that can be interpolated.
61557 */
61558 Ext.define('Ext.chart.axis.layout.Continuous', {
61559 extend: Ext.chart.axis.layout.Layout ,
61560 alias: 'axisLayout.continuous',
61561 config: {
61562 adjustMinimumByMajorUnit: false,
61563 adjustMaximumByMajorUnit: false
61564 },
61565
61566 getCoordFor: function (value, field, idx, items) {
61567 return +value;
61568 },
61569
61570 //@inheritdoc
61571 snapEnds: function (context, min, max, estStepSize) {
61572 var segmenter = context.segmenter,
61573 axis = this.getAxis(),
61574 minimum = axis.getMinimum(),
61575 maximum = axis.getMaximum(),
61576 majorTickSteps = axis.getMajorTickSteps(),
61577 out = majorTickSteps && Ext.isNumber(minimum) && Ext.isNumber(maximum) && segmenter.exactStep ?
61578 segmenter.exactStep(min, (max - min) / majorTickSteps) :
61579 segmenter.preferredStep(min, estStepSize),
61580 unit = out.unit,
61581 step = out.step,
61582 from = segmenter.align(min, step, unit),
61583 steps = segmenter.diff(min, max, unit) + 1;
61584 return {
61585 min: segmenter.from(min),
61586 max: segmenter.from(max),
61587 from: from,
61588 to: segmenter.add(from, steps * step, unit),
61589 step: step,
61590 steps: steps,
61591 unit: unit,
61592 get: function (current) {
61593 return segmenter.add(this.from, this.step * current, unit);
61594 }
61595 };
61596 },
61597
61598 snapMinorEnds: function (context) {
61599 var majorTicks = context.majorTicks,
61600 minorTickSteps = this.getAxis().getMinorTickSteps(),
61601 segmenter = context.segmenter,
61602 min = majorTicks.min,
61603 max = majorTicks.max,
61604 from = majorTicks.from,
61605 unit = majorTicks.unit,
61606 step = majorTicks.step / minorTickSteps,
61607 scaledStep = step * unit.scale,
61608 fromMargin = from - min,
61609 offset = Math.floor(fromMargin / scaledStep),
61610 extraSteps = offset + Math.floor((max - majorTicks.to) / scaledStep) + 1,
61611 steps = majorTicks.steps * minorTickSteps + extraSteps;
61612 return {
61613 min: min,
61614 max: max,
61615 from: min + fromMargin % scaledStep,
61616 to: segmenter.add(from, steps * step, unit),
61617 step: step,
61618 steps: steps,
61619 unit: unit,
61620 get: function (current) {
61621 return (current % minorTickSteps + offset + 1 !== 0) ? // don't render minor tick in major tick position
61622 segmenter.add(this.from, this.step * current, unit) :
61623 null;
61624 }
61625 }
61626 }
61627 });
61628
61629 /**
61630 * @class Ext.chart.axis.layout.CombineDuplicate
61631 * @extends Ext.chart.axis.layout.Discrete
61632 *
61633 * Discrete processor that combines duplicate data points.
61634 */
61635 Ext.define("Ext.chart.axis.layout.CombineDuplicate", {
61636 extend: Ext.chart.axis.layout.Discrete ,
61637 alias: 'axisLayout.combineDuplicate',
61638
61639 getCoordFor: function (value, field, idx, items) {
61640 if (!(value in this.labelMap)) {
61641 var result = this.labelMap[value] = this.labels.length;
61642 this.labels.push(value);
61643 return result;
61644 }
61645 return this.labelMap[value];
61646 }
61647
61648 });
61649
61650 /**
61651 * @class Ext.chart.axis.Axis
61652 *
61653 * Defines axis for charts.
61654 *
61655 * Using the current model, the type of axis can be easily extended. By default, Sencha Touch provides three different
61656 * types of axis:
61657 *
61658 * * **numeric** - the data attached with this axes are considered to be numeric and continuous.
61659 * * **time** - the data attached with this axes are considered (or get converted into) date/time and they are continuous.
61660 * * **category** - the data attached with this axes conforms a finite set. They will be evenly placed on the axis and displayed in the same form they were provided.
61661 *
61662 * The behavior of an axis can be easily changed by setting different types of axis layout and axis segmenter to the axis.
61663 *
61664 * Axis layout defines how the data points are placed. Using continuous layout, the data points will be distributed by
61665 * the numeric value. Using discrete layout the data points will be spaced evenly. Furthermore, if you want to combine
61666 * the data points with the duplicate values in a discrete layout, you should use combineDuplicate layout.
61667 *
61668 * Segmenter defines the way to segment data range. For example, if you have a Date-type data range from Jan 1, 1997 to
61669 * Jan 1, 2017, the segmenter will segement the data range into years, months or days based on the current zooming
61670 * level.
61671 *
61672 * It is possible to write custom axis layouts and segmenters to extends this behavior by simply implementing interfaces
61673 * {@link Ext.chart.axis.layout.Layout} and {@link Ext.chart.axis.segmenter.Segmenter}.
61674 *
61675 * Here's an example for the axes part of a chart definition:
61676 * An example of axis for a series (in this case for an area chart that has multiple layers of yFields) could be:
61677 *
61678 * axes: [{
61679 * type: 'numeric',
61680 * position: 'left',
61681 * title: 'Number of Hits',
61682 * grid: {
61683 * odd: {
61684 * opacity: 1,
61685 * fill: '#ddd',
61686 * stroke: '#bbb',
61687 * lineWidth: 1
61688 * }
61689 * },
61690 * minimum: 0
61691 * }, {
61692 * type: 'category',
61693 * position: 'bottom',
61694 * title: 'Month of the Year',
61695 * grid: true,
61696 * label: {
61697 * rotate: {
61698 * degrees: 315
61699 * }
61700 * }
61701 * }]
61702 *
61703 * In this case we use a `numeric` axis for displaying the values of the Area series and a `category` axis for displaying the names of
61704 * the store elements. The numeric axis is placed on the left of the screen, while the category axis is placed at the bottom of the chart.
61705 * Both the category and numeric axes have `grid` set, which means that horizontal and vertical lines will cover the chart background. In the
61706 * category axis the labels will be rotated so they can fit the space better.
61707 */
61708 Ext.define('Ext.chart.axis.Axis', {
61709 xtype: 'axis',
61710
61711 mixins: {
61712 observable: Ext.mixin.Observable
61713 },
61714
61715
61716
61717
61718
61719
61720
61721 config: {
61722 /**
61723 * @cfg {String} position
61724 * Where to set the axis. Available options are `left`, `bottom`, `right`, `top`, `radial` and `angular`.
61725 */
61726 position: 'bottom',
61727
61728 /**
61729 * @cfg {Array} fields
61730 * An array containing the names of the record fields which should be mapped along the axis.
61731 * This is optional if the binding between series and fields is clear.
61732 */
61733 fields: [],
61734
61735 /**
61736 * @cfg {Object} label
61737 *
61738 * The label configuration object for the Axis. This object may include style attributes
61739 * like `spacing`, `padding`, `font` that receives a string or number and
61740 * returns a new string with the modified values.
61741 *
61742 * For more supported values, see the configurations for {@link Ext.chart.label.Label}.
61743 */
61744 label: { x: 0, y: 0, textBaseline: 'middle', textAlign: 'center', fontSize: 12, fontFamily: 'Helvetica' },
61745
61746 /**
61747 * @cfg {Object} grid
61748 * The grid configuration object for the Axis style. Can contain `stroke` or `fill` attributes.
61749 * Also may contain an `odd` or `even` property in which you only style things on odd or even rows.
61750 * For example:
61751 *
61752 *
61753 * grid {
61754 * odd: {
61755 * stroke: '#555'
61756 * },
61757 * even: {
61758 * stroke: '#ccc'
61759 * }
61760 * }
61761 */
61762 grid: false,
61763
61764 /**
61765 * @cfg {Function} renderer Allows direct customisation of rendered axis sprites.
61766 * @param {String} label The label.
61767 * @param {Object|Ext.chart.axis.layout.Layout} layout The layout configuration used by the axis.
61768 * @param {String} lastLabel The last label.
61769 * @return {String} The label to display.
61770 */
61771 renderer: null,
61772
61773 /**
61774 * @protected
61775 * @cfg {Ext.chart.AbstractChart} chart The Chart that the Axis is bound.
61776 */
61777 chart: null,
61778
61779 /**
61780 * @cfg {Object} style
61781 * The style for the axis line and ticks.
61782 * Refer to the {@link Ext.chart.axis.sprite.Axis}
61783 */
61784 style: null,
61785
61786 /**
61787 * @cfg {Number} titleMargin
61788 * The margin around the axis title. Unlike CSS where the margin is added on all 4
61789 * sides of an element, the `titleMargin` is the total space that is added horizontally
61790 * for a vertical title and vertically for an horizontal title, with half the `titleMargin`
61791 * being added on either side.
61792 */
61793 titleMargin: 4,
61794
61795 /**
61796 * @cfg {Object} background
61797 * The background config for the axis surface.
61798 */
61799 background: null,
61800
61801 /**
61802 * @cfg {Number} minimum
61803 * The minimum value drawn by the axis. If not set explicitly, the axis
61804 * minimum will be calculated automatically.
61805 */
61806 minimum: NaN,
61807
61808 /**
61809 * @cfg {Number} maximum
61810 * The maximum value drawn by the axis. If not set explicitly, the axis
61811 * maximum will be calculated automatically.
61812 */
61813 maximum: NaN,
61814
61815 /**
61816 * @cfg {Number} minZoom
61817 * The minimum zooming level for axis.
61818 */
61819 minZoom: 1,
61820
61821 /**
61822 * @cfg {Number} maxZoom
61823 * The maximum zooming level for axis
61824 */
61825 maxZoom: 10000,
61826
61827 /**
61828 * @cfg {Object|Ext.chart.axis.layout.Layout} layout
61829 * The axis layout config. See {@link Ext.chart.axis.layout.Layout}
61830 */
61831 layout: 'continuous',
61832
61833 /**
61834 * @cfg {Object|Ext.chart.axis.segmenter.Segmenter} segmenter
61835 * The segmenter config. See {@link Ext.chart.axis.segmenter.Segmenter}
61836 */
61837 segmenter: 'numeric',
61838
61839 /**
61840 * @cfg {Boolean} hidden
61841 * Indicate whether to hide the axis.
61842 * If the axis is hidden, one of the axis line, ticks, labels or the title will be shown and
61843 * no margin will be taken.
61844 * The coordination mechanism works fine no matter if the axis is hidden.
61845 */
61846 hidden: false,
61847
61848 /**
61849 * @cfg {Number} majorTickSteps
61850 * If `minimum` and `maximum` are specified it forces the number of major ticks to the specified value.
61851 */
61852 majorTickSteps: false,
61853
61854 /**
61855 * @cfg {Number} [minorTickSteps=0]
61856 * The number of small ticks between two major ticks.
61857 */
61858 minorTickSteps: false,
61859
61860 /**
61861 * @private
61862 * @cfg {Boolean} adjustMaximumByMajorUnit
61863 * Will be supported soon.
61864 */
61865 adjustMaximumByMajorUnit: false,
61866
61867 /**
61868 * @private
61869 * @cfg {Boolean} adjustMinimumByMajorUnit
61870 * Will be supported soon.
61871 *
61872 */
61873 adjustMinimumByMajorUnit: false,
61874
61875 /**
61876 * @cfg {String|Object} title
61877 * The title for the Axis.
61878 * If given a String, the text style of the title sprite will be set,
61879 * otherwise the style will be set.
61880 */
61881 title: { fontSize: 18, fontFamily: 'Helvetica'},
61882
61883 /**
61884 * @cfg {Number} increment
61885 * Given a minimum and maximum bound for the series to be rendered (that can be obtained
61886 * automatically or by manually setting `minimum` and `maximum`) tick marks will be added
61887 * on each `increment` from the minimum value to the maximum one.
61888 */
61889 increment: 0.5,
61890
61891 /**
61892 * @private
61893 * @cfg {Number} length
61894 * Length of the axis position. Equals to the size of inner region on the docking side of this axis.
61895 * WARNING: Meant to be set automatically by chart. Do not set it manually.
61896 */
61897 length: 0,
61898
61899 /**
61900 * @private
61901 * @cfg {Array} center
61902 * Center of the polar axis.
61903 * WARNING: Meant to be set automatically by chart. Do not set it manually.
61904 */
61905 center: null,
61906
61907 /**
61908 * @private
61909 * @cfg {Number} radius
61910 * Radius of the polar axis.
61911 * WARNING: Meant to be set automatically by chart. Do not set it manually.
61912 */
61913 radius: null,
61914
61915 /**
61916 * @private
61917 * @cfg {Number} rotation
61918 * Rotation of the polar axis.
61919 * WARNING: Meant to be set automatically by chart. Do not set it manually.
61920 */
61921 rotation: null,
61922
61923 /**
61924 * @cfg {Boolean} [labelInSpan]
61925 * Draws the labels in the middle of the spans.
61926 */
61927 labelInSpan: null,
61928
61929 /**
61930 * @cfg {Array} visibleRange
61931 * Specify the proportion of the axis to be rendered. The series bound to
61932 * this axis will be synchronized and transformed.
61933 */
61934 visibleRange: [0, 1],
61935
61936 /**
61937 * @cfg {Boolean} needHighPrecision
61938 * Indicates that the axis needs high precision surface implementation.
61939 * See {@link Ext.draw.engine.Canvas#highPrecision}
61940 */
61941 needHighPrecision: false
61942 },
61943
61944 observableType: 'component',
61945
61946 titleOffset: 0,
61947
61948 animating: 0,
61949
61950 prevMin: 0,
61951
61952 prevMax: 1,
61953
61954 boundSeries: [],
61955
61956 sprites: null,
61957
61958 /**
61959 * @private
61960 * @property {Array} The full data range of the axis. Should not be set directly, clear it to `null` and use
61961 * `getRange` to update.
61962 */
61963 range: null,
61964
61965 xValues: [],
61966
61967 yValues: [],
61968
61969 applyRotation: function (rotation) {
61970 var twoPie = Math.PI * 2;
61971 return (rotation % twoPie + Math.PI) % twoPie - Math.PI;
61972 },
61973
61974 updateRotation: function (rotation) {
61975 var sprites = this.getSprites(),
61976 position = this.getPosition();
61977 if (!this.getHidden() && position === 'angular' && sprites[0]) {
61978 sprites[0].setAttributes({
61979 baseRotation: rotation
61980 });
61981 }
61982 },
61983
61984 applyTitle: function (title, oldTitle) {
61985 var surface;
61986
61987 if (Ext.isString(title)) {
61988 title = { text: title };
61989 }
61990
61991 if (!oldTitle) {
61992 oldTitle = Ext.create('sprite.text', title);
61993 if ((surface = this.getSurface())) {
61994 surface.add(oldTitle);
61995 }
61996 } else {
61997 oldTitle.setAttributes(title);
61998 }
61999 return oldTitle;
62000 },
62001
62002 constructor: function (config) {
62003 var me = this;
62004 me.sprites = [];
62005 this.labels = [];
62006 this.initConfig(config);
62007 me.getId();
62008 me.mixins.observable.constructor.apply(me, arguments);
62009 Ext.ComponentManager.register(me);
62010 },
62011
62012 /**
62013 * @private
62014 * @return {String}
62015 */
62016 getAlignment: function () {
62017 switch (this.getPosition()) {
62018 case 'left':
62019 case 'right':
62020 return 'vertical';
62021 case 'top':
62022 case 'bottom':
62023 return 'horizontal';
62024 case 'radial':
62025 return 'radial';
62026 case 'angular':
62027 return 'angular';
62028 }
62029 },
62030
62031 /**
62032 * @private
62033 * @return {String}
62034 */
62035 getGridAlignment: function () {
62036 switch (this.getPosition()) {
62037 case 'left':
62038 case 'right':
62039 return 'horizontal';
62040 case 'top':
62041 case 'bottom':
62042 return 'vertical';
62043 case 'radial':
62044 return 'circular';
62045 case 'angular':
62046 return "radial";
62047 }
62048 },
62049
62050 /**
62051 * @private
62052 * Get the surface for drawing the series sprites
62053 */
62054 getSurface: function () {
62055 if (!this.surface) {
62056 var chart = this.getChart();
62057 if (!chart) {
62058 return null;
62059 }
62060 var surface = this.surface = chart.getSurface(this.getId(), 'axis'),
62061 gridSurface = this.gridSurface = chart.getSurface('main'),
62062 sprites = this.getSprites(),
62063 sprite = sprites[0],
62064 grid = this.getGrid(),
62065 gridAlignment = this.getGridAlignment(),
62066 gridSprite;
62067 if (grid) {
62068 gridSprite = this.gridSpriteEven = new Ext.chart.Markers();
62069 gridSprite.setTemplate({xclass: 'grid.' + gridAlignment});
62070 if (Ext.isObject(grid)) {
62071 gridSprite.getTemplate().setAttributes(grid);
62072 if (Ext.isObject(grid.even)) {
62073 gridSprite.getTemplate().setAttributes(grid.even);
62074 }
62075 }
62076 gridSurface.add(gridSprite);
62077 sprite.bindMarker(gridAlignment + '-even', gridSprite);
62078
62079 gridSprite = this.gridSpriteOdd = new Ext.chart.Markers();
62080 gridSprite.setTemplate({xclass: 'grid.' + gridAlignment});
62081 if (Ext.isObject(grid)) {
62082 gridSprite.getTemplate().setAttributes(grid);
62083 if (Ext.isObject(grid.odd)) {
62084 gridSprite.getTemplate().setAttributes(grid.odd);
62085 }
62086 }
62087 gridSurface.add(gridSprite);
62088 sprite.bindMarker(gridAlignment + '-odd', gridSprite);
62089
62090 gridSurface.waitFor(surface);
62091 }
62092 }
62093 return this.surface;
62094 },
62095
62096 /**
62097 *
62098 * Mapping data value into coordinate.
62099 *
62100 * @param {*} value
62101 * @param {String} field
62102 * @param {Number} [idx]
62103 * @param {Ext.util.MixedCollection} [items]
62104 * @return {Number}
62105 */
62106 getCoordFor: function (value, field, idx, items) {
62107 return this.getLayout().getCoordFor(value, field, idx, items);
62108 },
62109
62110 applyPosition: function (pos) {
62111 return pos.toLowerCase();
62112 },
62113
62114 applyLabel: function (newText, oldText) {
62115 if (!oldText) {
62116 oldText = new Ext.draw.sprite.Text({});
62117 }
62118 oldText.setAttributes(newText);
62119 return oldText;
62120 },
62121
62122 applyLayout: function (layout, oldLayout) {
62123 // TODO: finish this
62124 layout = Ext.factory(layout, null, oldLayout, 'axisLayout');
62125 layout.setAxis(this);
62126 return layout;
62127 },
62128
62129 applySegmenter: function (segmenter, oldSegmenter) {
62130 // TODO: finish this
62131 segmenter = Ext.factory(segmenter, null, oldSegmenter, 'segmenter');
62132 segmenter.setAxis(this);
62133 return segmenter;
62134 },
62135
62136 updateMinimum: function () {
62137 this.range = null;
62138 },
62139
62140 updateMaximum: function () {
62141 this.range = null;
62142 },
62143
62144 hideLabels: function () {
62145 this.getSprites()[0].setDirty(true);
62146 this.setLabel({hidden: true});
62147 },
62148
62149 showLabels: function () {
62150 this.getSprites()[0].setDirty(true);
62151 this.setLabel({hidden: false});
62152 },
62153
62154 /**
62155 * Invokes renderFrame on this axis's surface(s)
62156 */
62157 renderFrame: function () {
62158 this.getSurface().renderFrame();
62159 },
62160
62161 updateChart: function (newChart, oldChart) {
62162 var me = this, surface;
62163 if (oldChart) {
62164 oldChart.un('serieschanged', me.onSeriesChanged, me);
62165 }
62166 if (newChart) {
62167 newChart.on('serieschanged', me.onSeriesChanged, me);
62168 if (newChart.getSeries()) {
62169 me.onSeriesChanged(newChart);
62170 }
62171 me.surface = null;
62172 surface = me.getSurface();
62173 surface.add(me.getSprites());
62174 surface.add(me.getTitle());
62175 }
62176 },
62177
62178 applyBackground: function (background) {
62179 var rect = Ext.ClassManager.getByAlias('sprite.rect');
62180 return rect.def.normalize(background);
62181 },
62182
62183 /**
62184 * @protected
62185 * Invoked when data has changed.
62186 */
62187 processData: function () {
62188 this.getLayout().processData();
62189 this.range = null;
62190 },
62191
62192 getDirection: function () {
62193 return this.getChart().getDirectionForAxis(this.getPosition());
62194 },
62195
62196 isSide: function () {
62197 var position = this.getPosition();
62198 return position === 'left' || position === 'right';
62199 },
62200
62201 applyFields: function (fields) {
62202 return [].concat(fields);
62203 },
62204
62205 updateFields: function (fields) {
62206 this.fieldsMap = {};
62207 for (var i = 0; i < fields.length; i++) {
62208 this.fieldsMap[fields[i]] = true;
62209 }
62210 },
62211
62212 applyVisibleRange: function (visibleRange, oldVisibleRange) {
62213 // If it is in reversed order swap them
62214 if (visibleRange[0] > visibleRange[1]) {
62215 var temp = visibleRange[0];
62216 visibleRange[0] = visibleRange[1];
62217 visibleRange[0] = temp;
62218 }
62219 if (visibleRange[1] === visibleRange[0]) {
62220 visibleRange[1] += 1 / this.getMaxZoom();
62221 }
62222 if (visibleRange[1] > visibleRange[0] + 1) {
62223 visibleRange[0] = 0;
62224 visibleRange[1] = 1;
62225 } else if (visibleRange[0] < 0) {
62226 visibleRange[1] -= visibleRange[0];
62227 visibleRange[0] = 0;
62228 } else if (visibleRange[1] > 1) {
62229 visibleRange[0] -= visibleRange[1] - 1;
62230 visibleRange[1] = 1;
62231 }
62232
62233 if (oldVisibleRange && visibleRange[0] === oldVisibleRange[0] && visibleRange[1] === oldVisibleRange[1]) {
62234 return undefined;
62235 }
62236
62237 return visibleRange;
62238 },
62239
62240 updateVisibleRange: function (visibleRange) {
62241 this.fireEvent('transformed', this, visibleRange);
62242 },
62243
62244 onSeriesChanged: function (chart) {
62245 var me = this,
62246 series = chart.getSeries(),
62247 getAxisMethod = 'get' + me.getDirection() + 'Axis',
62248 boundSeries = [], i, ln = series.length;
62249 for (i = 0; i < ln; i++) {
62250 if (this === series[i][getAxisMethod]()) {
62251 boundSeries.push(series[i]);
62252 }
62253 }
62254
62255 me.boundSeries = boundSeries;
62256 me.getLayout().processData();
62257 },
62258
62259 applyRange: function (newRange) {
62260 if (!newRange) {
62261 return this.dataRange.slice(0);
62262 } else {
62263 return [
62264 newRange[0] === null ? this.dataRange[0] : newRange[0],
62265 newRange[1] === null ? this.dataRange[1] : newRange[1]
62266 ];
62267 }
62268 },
62269
62270 /**
62271 * Get the range derived from all the bound series.
62272 * @return {Array}
62273 */
62274 getRange: function () {
62275 var me = this,
62276 getRangeMethod = 'get' + me.getDirection() + 'Range';
62277
62278 if (me.range) {
62279 return me.range;
62280 }
62281 if (!isNaN(me.getMinimum()) && !isNaN(me.getMaximum())) {
62282 return this.range = [me.getMinimum(), me.getMaximum()];
62283 }
62284 var min = Infinity,
62285 max = -Infinity,
62286 boundSeries = me.boundSeries,
62287 series, i, ln;
62288
62289 // For each series bound to this axis, ask the series for its min/max values
62290 // and use them to find the overall min/max.
62291 for (i = 0, ln = boundSeries.length; i < ln; i++) {
62292 series = boundSeries[i];
62293 var minMax = series[getRangeMethod]();
62294
62295 if (minMax) {
62296 if (minMax[0] < min) {
62297 min = minMax[0];
62298 }
62299 if (minMax[1] > max) {
62300 max = minMax[1];
62301 }
62302 }
62303 }
62304 if (!isFinite(max)) {
62305 max = me.prevMax;
62306 }
62307
62308 if (!isFinite(min)) {
62309 min = me.prevMin;
62310 }
62311
62312 if (this.getLabelInSpan() || min === max) {
62313 max += this.getIncrement();
62314 min -= this.getIncrement();
62315 }
62316
62317 if (!isNaN(me.getMinimum())) {
62318 min = me.getMinimum();
62319 } else {
62320 me.prevMin = min;
62321 }
62322
62323 if (!isNaN(me.getMaximum())) {
62324 max = me.getMaximum();
62325 } else {
62326 me.prevMax = max;
62327 }
62328
62329 return this.range = [min, max];
62330 },
62331
62332 applyStyle: function (style, oldStyle) {
62333 var cls = Ext.ClassManager.getByAlias('sprite.' + this.seriesType);
62334 if (cls && cls.def) {
62335 style = cls.def.normalize(style);
62336 }
62337 oldStyle = Ext.apply(oldStyle || {}, style);
62338 return oldStyle;
62339 },
62340
62341 updateCenter: function (center) {
62342 var sprites = this.getSprites(),
62343 axisSprite = sprites[0],
62344 centerX = center[0],
62345 centerY = center[1];
62346 if (axisSprite) {
62347 axisSprite.setAttributes({
62348 centerX: centerX,
62349 centerY: centerY
62350 });
62351 }
62352 if (this.gridSpriteEven) {
62353 this.gridSpriteEven.getTemplate().setAttributes({
62354 translationX: centerX,
62355 translationY: centerY,
62356 rotationCenterX: centerX,
62357 rotationCenterY: centerY
62358 });
62359 }
62360 if (this.gridSpriteOdd) {
62361 this.gridSpriteOdd.getTemplate().setAttributes({
62362 translationX: centerX,
62363 translationY: centerY,
62364 rotationCenterX: centerX,
62365 rotationCenterY: centerY
62366 });
62367 }
62368 },
62369
62370 getSprites: function () {
62371 if (!this.getChart()) {
62372 return;
62373 }
62374 var me = this,
62375 range = me.getRange(),
62376 position = me.getPosition(),
62377 chart = me.getChart(),
62378 animation = chart.getAnimate(),
62379 baseSprite, style,
62380 length = me.getLength();
62381
62382 // If animation is false, then stop animation.
62383 if (animation === false) {
62384 animation = {
62385 duration: 0
62386 };
62387 }
62388 if (range) {
62389 style = Ext.applyIf({
62390 position: position,
62391 axis: me,
62392 min: range[0],
62393 max: range[1],
62394 length: length,
62395 grid: me.getGrid(),
62396 hidden: me.getHidden(),
62397 titleOffset: me.titleOffset,
62398 layout: me.getLayout(),
62399 segmenter: me.getSegmenter(),
62400 label: me.getLabel()
62401 }, me.getStyle());
62402
62403 // If the sprites are not created.
62404 if (!me.sprites.length) {
62405 baseSprite = new Ext.chart.axis.sprite.Axis(style);
62406 baseSprite.fx.setCustomDuration({
62407 baseRotation: 0
62408 });
62409 baseSprite.fx.on("animationstart", "onAnimationStart", me);
62410 baseSprite.fx.on("animationend", "onAnimationEnd", me);
62411 me.sprites.push(baseSprite);
62412 me.updateTitleSprite();
62413 } else {
62414 baseSprite = me.sprites[0];
62415 baseSprite.fx.setConfig(animation);
62416 baseSprite.setAttributes(style);
62417 baseSprite.setLayout(me.getLayout());
62418 baseSprite.setSegmenter(me.getSegmenter());
62419 baseSprite.setLabel(me.getLabel());
62420 }
62421
62422 if (me.getRenderer()) {
62423 baseSprite.setRenderer(me.getRenderer());
62424 }
62425 }
62426
62427 return me.sprites;
62428 },
62429
62430 updateTitleSprite: function () {
62431 if (!this.sprites[0]) {
62432 return;
62433 }
62434 var me = this,
62435 thickness = this.sprites[0].thickness,
62436 surface = me.getSurface(),
62437 title = this.getTitle(),
62438 position = me.getPosition(),
62439 titleMargin = me.getTitleMargin(),
62440 length = me.getLength(),
62441 anchor = surface.roundPixel(length / 2);
62442
62443 if (title) {
62444 switch (position) {
62445 case 'top':
62446 title.setAttributes({
62447 x: anchor,
62448 y: titleMargin / 2,
62449 textBaseline: 'top',
62450 textAlign: 'center'
62451 }, true, true);
62452 title.applyTransformations();
62453 me.titleOffset = title.getBBox().height + titleMargin;
62454 break;
62455 case 'bottom':
62456 title.setAttributes({
62457 x: anchor,
62458 y: thickness + titleMargin / 2,
62459 textBaseline: 'top',
62460 textAlign: 'center'
62461 }, true, true);
62462 title.applyTransformations();
62463 me.titleOffset = title.getBBox().height + titleMargin;
62464 break;
62465 case 'left':
62466 title.setAttributes({
62467 x: titleMargin / 2,
62468 y: anchor,
62469 textBaseline: 'top',
62470 textAlign: 'center',
62471 rotationCenterX: titleMargin / 2,
62472 rotationCenterY: anchor,
62473 rotationRads: -Math.PI / 2
62474 }, true, true);
62475 title.applyTransformations();
62476 me.titleOffset = title.getBBox().width + titleMargin;
62477 break;
62478 case 'right':
62479 title.setAttributes({
62480 x: thickness + titleMargin / 2,
62481 y: anchor,
62482 textBaseline: 'bottom',
62483 textAlign: 'center',
62484 rotationCenterX: thickness + titleMargin / 2,
62485 rotationCenterY: anchor,
62486 rotationRads: Math.PI / 2
62487 }, true, true);
62488 title.applyTransformations();
62489 me.titleOffset = title.getBBox().width + titleMargin;
62490 break;
62491 }
62492 }
62493 },
62494
62495 onThicknessChanged: function () {
62496 var me = this;
62497 me.getChart().onThicknessChanged();
62498 },
62499
62500 getThickness: function () {
62501 if (this.getHidden()) {
62502 return 0;
62503 }
62504 return (this.sprites[0] && this.sprites[0].thickness || 1) + this.titleOffset;
62505 },
62506
62507 onAnimationStart: function () {
62508 this.animating++;
62509 if (this.animating === 1) {
62510 this.fireEvent("animationstart");
62511 }
62512 },
62513
62514 onAnimationEnd: function () {
62515 this.animating--;
62516 if (this.animating === 0) {
62517 this.fireEvent("animationend");
62518 }
62519 },
62520
62521 // Methods used in ComponentQuery and controller
62522 getItemId: function () {
62523 return this.getId();
62524 },
62525
62526 getAncestorIds: function () {
62527 return [this.getChart().getId()];
62528 },
62529
62530 isXType: function (xtype) {
62531 return xtype === 'axis';
62532 },
62533
62534 destroy: function () {
62535 Ext.ComponentManager.unregister(this);
62536 this.callSuper();
62537 }
62538 });
62539
62540
62541 /**
62542 * @private
62543 */
62544 Ext.define('Ext.mixin.Sortable', {
62545 extend: Ext.mixin.Mixin ,
62546
62547
62548
62549
62550
62551 mixinConfig: {
62552 id: 'sortable'
62553 },
62554
62555 config: {
62556 /**
62557 * @cfg {Array} sorters
62558 * An array with sorters. A sorter can be an instance of {@link Ext.util.Sorter}, a string
62559 * indicating a property name, an object representing an Ext.util.Sorter configuration,
62560 * or a sort function.
62561 */
62562 sorters: null,
62563
62564 /**
62565 * @cfg {String} defaultSortDirection
62566 * The default sort direction to use if one is not specified.
62567 */
62568 defaultSortDirection: "ASC",
62569
62570 /**
62571 * @cfg {String} sortRoot
62572 * The root inside each item in which the properties exist that we want to sort on.
62573 * This is useful for sorting records in which the data exists inside a `data` property.
62574 */
62575 sortRoot: null
62576 },
62577
62578 /**
62579 * @property {Boolean} dirtySortFn
62580 * A flag indicating whether the currently cashed sort function is still valid.
62581 * @readonly
62582 */
62583 dirtySortFn: false,
62584
62585 /**
62586 * @property currentSortFn
62587 * This is the cached sorting function which is a generated function that calls all the
62588 * configured sorters in the correct order.
62589 * @readonly
62590 */
62591 sortFn: null,
62592
62593 /**
62594 * @property {Boolean} sorted
62595 * A read-only flag indicating if this object is sorted.
62596 * @readonly
62597 */
62598 sorted: false,
62599
62600 applySorters: function(sorters, collection) {
62601 if (!collection) {
62602 collection = this.createSortersCollection();
62603 }
62604
62605 collection.clear();
62606 this.sorted = false;
62607
62608 if (sorters) {
62609 this.addSorters(sorters);
62610 }
62611
62612 return collection;
62613 },
62614
62615 createSortersCollection: function() {
62616 this._sorters = Ext.create('Ext.util.Collection', function(sorter) {
62617 return sorter.getId();
62618 });
62619 return this._sorters;
62620 },
62621
62622 /**
62623 * This method adds a sorter.
62624 * @param {Ext.util.Sorter/String/Function/Object} sorter Can be an instance of
62625 * Ext.util.Sorter, a string indicating a property name, an object representing an Ext.util.Sorter
62626 * configuration, or a sort function.
62627 * @param {String} defaultDirection The default direction for each sorter in the array. Defaults
62628 * to the value of {@link #defaultSortDirection}. Can be either 'ASC' or 'DESC'.
62629 */
62630 addSorter: function(sorter, defaultDirection) {
62631 this.addSorters([sorter], defaultDirection);
62632 },
62633
62634 /**
62635 * This method adds all the sorters in a passed array.
62636 * @param {Array} sorters An array with sorters. A sorter can be an instance of Ext.util.Sorter, a string
62637 * indicating a property name, an object representing an Ext.util.Sorter configuration,
62638 * or a sort function.
62639 * @param {String} defaultDirection The default direction for each sorter in the array. Defaults
62640 * to the value of {@link #defaultSortDirection}. Can be either 'ASC' or 'DESC'.
62641 */
62642 addSorters: function(sorters, defaultDirection) {
62643 var currentSorters = this.getSorters();
62644 return this.insertSorters(currentSorters ? currentSorters.length : 0, sorters, defaultDirection);
62645 },
62646
62647 /**
62648 * This method adds a sorter at a given index.
62649 * @param {Number} index The index at which to insert the sorter.
62650 * @param {Ext.util.Sorter/String/Function/Object} sorter Can be an instance of Ext.util.Sorter,
62651 * a string indicating a property name, an object representing an Ext.util.Sorter configuration,
62652 * or a sort function.
62653 * @param {String} defaultDirection The default direction for each sorter in the array. Defaults
62654 * to the value of {@link #defaultSortDirection}. Can be either 'ASC' or 'DESC'.
62655 */
62656 insertSorter: function(index, sorter, defaultDirection) {
62657 return this.insertSorters(index, [sorter], defaultDirection);
62658 },
62659
62660 /**
62661 * This method inserts all the sorters in the passed array at the given index.
62662 * @param {Number} index The index at which to insert the sorters.
62663 * @param {Array} sorters Can be an instance of Ext.util.Sorter, a string indicating a property name,
62664 * an object representing an Ext.util.Sorter configuration, or a sort function.
62665 * @param {String} defaultDirection The default direction for each sorter in the array. Defaults
62666 * to the value of {@link #defaultSortDirection}. Can be either 'ASC' or 'DESC'.
62667 */
62668 insertSorters: function(index, sorters, defaultDirection) {
62669 // We begin by making sure we are dealing with an array of sorters
62670 if (!Ext.isArray(sorters)) {
62671 sorters = [sorters];
62672 }
62673
62674 var ln = sorters.length,
62675 direction = defaultDirection || this.getDefaultSortDirection(),
62676 sortRoot = this.getSortRoot(),
62677 currentSorters = this.getSorters(),
62678 newSorters = [],
62679 sorterConfig, i, sorter, currentSorter;
62680
62681 if (!currentSorters) {
62682 // This will guarantee that we get the collection
62683 currentSorters = this.createSortersCollection();
62684 }
62685
62686 // We first have to convert every sorter into a proper Sorter instance
62687 for (i = 0; i < ln; i++) {
62688 sorter = sorters[i];
62689 sorterConfig = {
62690 direction: direction,
62691 root: sortRoot
62692 };
62693
62694 // If we are dealing with a string we assume it is a property they want to sort on.
62695 if (typeof sorter === 'string') {
62696 currentSorter = currentSorters.get(sorter);
62697
62698 if (!currentSorter) {
62699 sorterConfig.property = sorter;
62700 } else {
62701 if (defaultDirection) {
62702 currentSorter.setDirection(defaultDirection);
62703 } else {
62704 // If we already have a sorter for this property we just toggle its direction.
62705 currentSorter.toggle();
62706 }
62707 continue;
62708 }
62709 }
62710 // If it is a function, we assume its a sorting function.
62711 else if (Ext.isFunction(sorter)) {
62712 sorterConfig.sorterFn = sorter;
62713 }
62714 // If we are dealing with an object, we assume its a Sorter configuration. In this case
62715 // we create an instance of Sorter passing this configuration.
62716 else if (Ext.isObject(sorter)) {
62717 if (!sorter.isSorter) {
62718 if (sorter.fn) {
62719 sorter.sorterFn = sorter.fn;
62720 delete sorter.fn;
62721 }
62722
62723 sorterConfig = Ext.apply(sorterConfig, sorter);
62724 }
62725 else {
62726 newSorters.push(sorter);
62727 if (!sorter.getRoot()) {
62728 sorter.setRoot(sortRoot);
62729 }
62730 continue;
62731 }
62732 }
62733 // Finally we get to the point where it has to be invalid
62734 // <debug>
62735 else {
62736 Ext.Logger.warn('Invalid sorter specified:', sorter);
62737 }
62738 // </debug>
62739
62740 // If a sorter config was created, make it an instance
62741 sorter = Ext.create('Ext.util.Sorter', sorterConfig);
62742 newSorters.push(sorter);
62743 }
62744
62745 // Now lets add the newly created sorters.
62746 for (i = 0, ln = newSorters.length; i < ln; i++) {
62747 currentSorters.insert(index + i, newSorters[i]);
62748 }
62749
62750 this.dirtySortFn = true;
62751
62752 if (currentSorters.length) {
62753 this.sorted = true;
62754 }
62755 return currentSorters;
62756 },
62757
62758 /**
62759 * This method removes a sorter.
62760 * @param {Ext.util.Sorter/String/Function/Object} sorter Can be an instance of Ext.util.Sorter,
62761 * a string indicating a property name, an object representing an Ext.util.Sorter configuration,
62762 * or a sort function.
62763 */
62764 removeSorter: function(sorter) {
62765 return this.removeSorters([sorter]);
62766 },
62767
62768 /**
62769 * This method removes all the sorters in a passed array.
62770 * @param {Array} sorters Each value in the array can be a string (property name),
62771 * function (sorterFn) or {@link Ext.util.Sorter Sorter} instance.
62772 */
62773 removeSorters: function(sorters) {
62774 // We begin by making sure we are dealing with an array of sorters
62775 if (!Ext.isArray(sorters)) {
62776 sorters = [sorters];
62777 }
62778
62779 var ln = sorters.length,
62780 currentSorters = this.getSorters(),
62781 i, sorter;
62782
62783 for (i = 0; i < ln; i++) {
62784 sorter = sorters[i];
62785
62786 if (typeof sorter === 'string') {
62787 currentSorters.removeAtKey(sorter);
62788 }
62789 else if (typeof sorter === 'function') {
62790 currentSorters.each(function(item) {
62791 if (item.getSorterFn() === sorter) {
62792 currentSorters.remove(item);
62793 }
62794 });
62795 }
62796 else if (sorter.isSorter) {
62797 currentSorters.remove(sorter);
62798 }
62799 }
62800
62801 if (!currentSorters.length) {
62802 this.sorted = false;
62803 }
62804 },
62805
62806 /**
62807 * This updates the cached sortFn based on the current sorters.
62808 * @return {Function} The generated sort function.
62809 * @private
62810 */
62811 updateSortFn: function() {
62812 var sorters = this.getSorters().items;
62813
62814 this.sortFn = function(r1, r2) {
62815 var ln = sorters.length,
62816 result, i;
62817
62818 // We loop over each sorter and check if r1 should be before or after r2
62819 for (i = 0; i < ln; i++) {
62820 result = sorters[i].sort.call(this, r1, r2);
62821
62822 // If the result is -1 or 1 at this point it means that the sort is done.
62823 // Only if they are equal (0) we continue to see if a next sort function
62824 // actually might find a winner.
62825 if (result !== 0) {
62826 break;
62827 }
62828 }
62829
62830 return result;
62831 };
62832
62833 this.dirtySortFn = false;
62834 return this.sortFn;
62835 },
62836
62837 /**
62838 * Returns an up to date sort function.
62839 * @return {Function} The sort function.
62840 */
62841 getSortFn: function() {
62842 if (this.dirtySortFn) {
62843 return this.updateSortFn();
62844 }
62845 return this.sortFn;
62846 },
62847
62848 /**
62849 * This method will sort an array based on the currently configured {@link #sorters}.
62850 * @param {Array} data The array you want to have sorted.
62851 * @return {Array} The array you passed after it is sorted.
62852 */
62853 sort: function(data) {
62854 Ext.Array.sort(data, this.getSortFn());
62855 return data;
62856 },
62857
62858 /**
62859 * This method returns the index that a given item would be inserted into a
62860 * given array based on the current sorters.
62861 * @param {Array} items The array that you want to insert the item into.
62862 * @param {Mixed} item The item that you want to insert into the items array.
62863 * @return {Number} The index for the given item in the given array based on
62864 * the current sorters.
62865 */
62866 findInsertionIndex: function(items, item, sortFn) {
62867 var start = 0,
62868 end = items.length - 1,
62869 sorterFn = sortFn || this.getSortFn(),
62870 middle,
62871 comparison;
62872
62873 while (start <= end) {
62874 middle = (start + end) >> 1;
62875 comparison = sorterFn(item, items[middle]);
62876 if (comparison >= 0) {
62877 start = middle + 1;
62878 } else if (comparison < 0) {
62879 end = middle - 1;
62880 }
62881 }
62882
62883 return start;
62884 }
62885 });
62886
62887 /**
62888 * @private
62889 */
62890 Ext.define('Ext.mixin.Filterable', {
62891 extend: Ext.mixin.Mixin ,
62892
62893
62894
62895
62896
62897 mixinConfig: {
62898 id: 'filterable'
62899 },
62900
62901 config: {
62902 /**
62903 * @cfg {Array} filters
62904 * An array with filters. A filter can be an instance of Ext.util.Filter,
62905 * an object representing an Ext.util.Filter configuration, or a filter function.
62906 */
62907 filters: null,
62908
62909 /**
62910 * @cfg {String} filterRoot
62911 * The root inside each item in which the properties exist that we want to filter on.
62912 * This is useful for filtering records in which the data exists inside a 'data' property.
62913 */
62914 filterRoot: null
62915 },
62916
62917 /**
62918 * @property {Boolean} dirtyFilterFn
62919 * A flag indicating whether the currently cashed filter function is still valid.
62920 * @readonly
62921 */
62922 dirtyFilterFn: false,
62923
62924 /**
62925 * @property currentSortFn
62926 * This is the cached sorting function which is a generated function that calls all the
62927 * configured sorters in the correct order.
62928 * @readonly
62929 */
62930 filterFn: null,
62931
62932 /**
62933 * @property {Boolean} filtered
62934 * A read-only flag indicating if this object is filtered.
62935 * @readonly
62936 */
62937 filtered: false,
62938
62939 applyFilters: function(filters, collection) {
62940 if (!collection) {
62941 collection = this.createFiltersCollection();
62942 }
62943
62944 collection.clear();
62945
62946 this.filtered = false;
62947 this.dirtyFilterFn = true;
62948
62949 if (filters) {
62950 this.addFilters(filters);
62951 }
62952
62953 return collection;
62954 },
62955
62956 createFiltersCollection: function() {
62957 this._filters = Ext.create('Ext.util.Collection', function(filter) {
62958 return filter.getId();
62959 });
62960 return this._filters;
62961 },
62962
62963 /**
62964 * This method adds a filter.
62965 * @param {Ext.util.Sorter/Function/Object} filter Can be an instance of Ext.util.Filter,
62966 * an object representing an Ext.util.Filter configuration, or a filter function.
62967 */
62968 addFilter: function(filter) {
62969 this.addFilters([filter]);
62970 },
62971
62972 /**
62973 * This method adds all the filters in a passed array.
62974 * @param {Array} filters An array with filters. A filter can be an instance of {@link Ext.util.Filter},
62975 * an object representing an Ext.util.Filter configuration, or a filter function.
62976 * @return {Object}
62977 */
62978 addFilters: function(filters) {
62979 var currentFilters = this.getFilters();
62980 return this.insertFilters(currentFilters ? currentFilters.length : 0, filters);
62981 },
62982
62983 /**
62984 * This method adds a filter at a given index.
62985 * @param {Number} index The index at which to insert the filter.
62986 * @param {Ext.util.Sorter/Function/Object} filter Can be an instance of {@link Ext.util.Filter},
62987 * an object representing an Ext.util.Filter configuration, or a filter function.
62988 * @return {Object}
62989 */
62990 insertFilter: function(index, filter) {
62991 return this.insertFilters(index, [filter]);
62992 },
62993
62994 /**
62995 * This method inserts all the filters in the passed array at the given index.
62996 * @param {Number} index The index at which to insert the filters.
62997 * @param {Array} filters Each filter can be an instance of {@link Ext.util.Filter},
62998 * an object representing an Ext.util.Filter configuration, or a filter function.
62999 * @return {Array}
63000 */
63001 insertFilters: function(index, filters) {
63002 // We begin by making sure we are dealing with an array of sorters
63003 if (!Ext.isArray(filters)) {
63004 filters = [filters];
63005 }
63006
63007 var ln = filters.length,
63008 filterRoot = this.getFilterRoot(),
63009 currentFilters = this.getFilters(),
63010 newFilters = [],
63011 filterConfig, i, filter;
63012
63013 if (!currentFilters) {
63014 currentFilters = this.createFiltersCollection();
63015 }
63016
63017 // We first have to convert every sorter into a proper Sorter instance
63018 for (i = 0; i < ln; i++) {
63019 filter = filters[i];
63020 filterConfig = {
63021 root: filterRoot
63022 };
63023
63024 if (Ext.isFunction(filter)) {
63025 filterConfig.filterFn = filter;
63026 }
63027 // If we are dealing with an object, we assume its a Sorter configuration. In this case
63028 // we create an instance of Sorter passing this configuration.
63029 else if (Ext.isObject(filter)) {
63030 if (!filter.isFilter) {
63031 if (filter.fn) {
63032 filter.filterFn = filter.fn;
63033 delete filter.fn;
63034 }
63035
63036 filterConfig = Ext.apply(filterConfig, filter);
63037 }
63038 else {
63039 newFilters.push(filter);
63040 if (!filter.getRoot()) {
63041 filter.setRoot(filterRoot);
63042 }
63043 continue;
63044 }
63045 }
63046 // Finally we get to the point where it has to be invalid
63047 // <debug>
63048 else {
63049 Ext.Logger.warn('Invalid filter specified:', filter);
63050 }
63051 // </debug>
63052
63053 // If a sorter config was created, make it an instance
63054 filter = Ext.create('Ext.util.Filter', filterConfig);
63055 newFilters.push(filter);
63056 }
63057
63058 // Now lets add the newly created sorters.
63059 for (i = 0, ln = newFilters.length; i < ln; i++) {
63060 currentFilters.insert(index + i, newFilters[i]);
63061 }
63062
63063 this.dirtyFilterFn = true;
63064
63065 if (currentFilters.length) {
63066 this.filtered = true;
63067 }
63068
63069 return currentFilters;
63070 },
63071
63072 /**
63073 * This method removes all the filters in a passed array.
63074 * @param {Array} filters Each value in the array can be a string (property name),
63075 * function (sorterFn), an object containing a property and value keys or
63076 * {@link Ext.util.Sorter Sorter} instance.
63077 */
63078 removeFilters: function(filters) {
63079 // We begin by making sure we are dealing with an array of sorters
63080 if (!Ext.isArray(filters)) {
63081 filters = [filters];
63082 }
63083
63084 var ln = filters.length,
63085 currentFilters = this.getFilters(),
63086 i, filter;
63087
63088 for (i = 0; i < ln; i++) {
63089 filter = filters[i];
63090
63091 if (typeof filter === 'string') {
63092 currentFilters.each(function(item) {
63093 if (item.getProperty() === filter) {
63094 currentFilters.remove(item);
63095 }
63096 });
63097 }
63098 else if (typeof filter === 'function') {
63099 currentFilters.each(function(item) {
63100 if (item.getFilterFn() === filter) {
63101 currentFilters.remove(item);
63102 }
63103 });
63104 }
63105 else {
63106 if (filter.isFilter) {
63107 currentFilters.remove(filter);
63108 }
63109 else if (filter.property !== undefined && filter.value !== undefined) {
63110 currentFilters.each(function(item) {
63111 if (item.getProperty() === filter.property && item.getValue() === filter.value) {
63112 currentFilters.remove(item);
63113 }
63114 });
63115 }
63116 }
63117 }
63118
63119 if (!currentFilters.length) {
63120 this.filtered = false;
63121 }
63122 },
63123
63124 /**
63125 * This updates the cached sortFn based on the current sorters.
63126 * @return {Function} sortFn The generated sort function.
63127 * @private
63128 */
63129 updateFilterFn: function() {
63130 var filters = this.getFilters().items;
63131
63132 this.filterFn = function(item) {
63133 var isMatch = true,
63134 length = filters.length,
63135 i;
63136
63137 for (i = 0; i < length; i++) {
63138 var filter = filters[i],
63139 fn = filter.getFilterFn(),
63140 scope = filter.getScope() || this;
63141
63142 isMatch = isMatch && fn.call(scope, item);
63143 }
63144
63145 return isMatch;
63146 };
63147
63148 this.dirtyFilterFn = false;
63149 return this.filterFn;
63150 },
63151
63152 /**
63153 * This method will sort an array based on the currently configured {@link Ext.data.Store#sorters sorters}.
63154 * @param {Array} data The array you want to have sorted.
63155 * @return {Array} The array you passed after it is sorted.
63156 */
63157 filter: function(data) {
63158 return this.getFilters().length ? Ext.Array.filter(data, this.getFilterFn()) : data;
63159 },
63160
63161 isFiltered: function(item) {
63162 return this.getFilters().length ? !this.getFilterFn()(item) : false;
63163 },
63164
63165 /**
63166 * Returns an up to date sort function.
63167 * @return {Function} sortFn The sort function.
63168 */
63169 getFilterFn: function() {
63170 if (this.dirtyFilterFn) {
63171 return this.updateFilterFn();
63172 }
63173 return this.filterFn;
63174 }
63175 });
63176
63177 /**
63178 * @private
63179 */
63180 Ext.define('Ext.util.Collection', {
63181 /**
63182 * @cfg {Object[]} filters
63183 * Array of {@link Ext.util.Filter Filters} for this collection.
63184 */
63185
63186 /**
63187 * @cfg {Object[]} sorters
63188 * Array of {@link Ext.util.Sorter Sorters} for this collection.
63189 */
63190
63191 config: {
63192 autoFilter: true,
63193 autoSort: true
63194 },
63195
63196 mixins: {
63197 sortable: Ext.mixin.Sortable ,
63198 filterable: Ext.mixin.Filterable
63199 },
63200
63201 constructor: function(keyFn, config) {
63202 var me = this;
63203
63204 /**
63205 * @property {Array} [all=[]]
63206 * An array containing all the items (unsorted, unfiltered)
63207 */
63208 me.all = [];
63209
63210 /**
63211 * @property {Array} [items=[]]
63212 * An array containing the filtered items (sorted)
63213 */
63214 me.items = [];
63215
63216 /**
63217 * @property {Array} [keys=[]]
63218 * An array containing all the filtered keys (sorted)
63219 */
63220 me.keys = [];
63221
63222 /**
63223 * @property {Object} [indices={}]
63224 * An object used as map to get a sorted and filtered index of an item
63225 */
63226 me.indices = {};
63227
63228 /**
63229 * @property {Object} [map={}]
63230 * An object used as map to get an object based on its key
63231 */
63232 me.map = {};
63233
63234 /**
63235 * @property {Number} [length=0]
63236 * The count of items in the collection filtered and sorted
63237 */
63238 me.length = 0;
63239
63240 if (keyFn) {
63241 me.getKey = keyFn;
63242 }
63243
63244 this.initConfig(config);
63245 },
63246
63247 updateAutoSort: function(autoSort, oldAutoSort) {
63248 if (oldAutoSort === false && autoSort && this.items.length) {
63249 this.sort();
63250 }
63251 },
63252
63253 updateAutoFilter: function(autoFilter, oldAutoFilter) {
63254 if (oldAutoFilter === false && autoFilter && this.all.length) {
63255 this.filter();
63256 }
63257 },
63258
63259 insertSorters: function() {
63260 // We override the insertSorters method that exists on the Sortable mixin. This method always
63261 // gets called whenever you add or insert a new sorter. We do this because we actually want
63262 // to sort right after this happens.
63263 this.mixins.sortable.insertSorters.apply(this, arguments);
63264 if (this.getAutoSort() && this.items.length) {
63265 this.sort();
63266 }
63267 return this;
63268 },
63269
63270 removeSorters: function(sorters) {
63271 // We override the removeSorters method that exists on the Sortable mixin. This method always
63272 // gets called whenever you remove a sorter. If we are still sorted after we removed this sorter,
63273 // then we have to resort the whole collection.
63274 this.mixins.sortable.removeSorters.call(this, sorters);
63275 if (this.sorted && this.getAutoSort() && this.items.length) {
63276 this.sort();
63277 }
63278 return this;
63279 },
63280
63281 applyFilters: function(filters) {
63282 var collection = this.mixins.filterable.applyFilters.call(this, filters);
63283 if (!filters && this.all.length && this.getAutoFilter()) {
63284 this.filter();
63285 }
63286 return collection;
63287 },
63288
63289 addFilters: function(filters) {
63290 // We override the insertFilters method that exists on the Filterable mixin. This method always
63291 // gets called whenever you add or insert a new filter. We do this because we actually want
63292 // to filter right after this happens.
63293 this.mixins.filterable.addFilters.call(this, filters);
63294 if (this.items.length && this.getAutoFilter()) {
63295 this.filter();
63296 }
63297 return this;
63298 },
63299
63300 removeFilters: function(filters) {
63301 // We override the removeFilters method that exists on the Filterable mixin. This method always
63302 // gets called whenever you remove a filter. If we are still filtered after we removed this filter,
63303 // then we have to re-filter the whole collection.
63304 this.mixins.filterable.removeFilters.call(this, filters);
63305 if (this.filtered && this.all.length && this.getAutoFilter()) {
63306 this.filter();
63307 }
63308 return this;
63309 },
63310
63311 /**
63312 * This method will sort a collection based on the currently configured sorters.
63313 * @param {Object} property
63314 * @param {Object} value
63315 * @param {Object} anyMatch
63316 * @param {Object} caseSensitive
63317 * @return {Array}
63318 */
63319 filter: function(property, value, anyMatch, caseSensitive) {
63320 // Support for the simple case of filtering by property/value
63321 if (property) {
63322 if (Ext.isString(property)) {
63323 this.addFilters({
63324 property : property,
63325 value : value,
63326 anyMatch : anyMatch,
63327 caseSensitive: caseSensitive
63328 });
63329 return this.items;
63330 }
63331 else {
63332 this.addFilters(property);
63333 return this.items;
63334 }
63335 }
63336
63337 this.items = this.mixins.filterable.filter.call(this, this.all.slice());
63338 this.updateAfterFilter();
63339
63340 if (this.sorted && this.getAutoSort()) {
63341 this.sort();
63342 }
63343 },
63344
63345 updateAfterFilter: function() {
63346 var items = this.items,
63347 keys = this.keys,
63348 indices = this.indices = {},
63349 ln = items.length,
63350 i, item, key;
63351
63352 keys.length = 0;
63353
63354 for (i = 0; i < ln; i++) {
63355 item = items[i];
63356 key = this.getKey(item);
63357 indices[key] = i;
63358 keys[i] = key;
63359 }
63360
63361 this.length = items.length;
63362 this.dirtyIndices = false;
63363 },
63364
63365 sort: function(sorters, defaultDirection) {
63366 var items = this.items,
63367 keys = this.keys,
63368 indices = this.indices,
63369 ln = items.length,
63370 i, item, key;
63371
63372 // If we pass sorters to this method we have to add them first.
63373 // Because adding a sorter automatically sorts the items collection
63374 // we can just return items after we have added the sorters
63375 if (sorters) {
63376 this.addSorters(sorters, defaultDirection);
63377 return this.items;
63378 }
63379
63380 // We save the keys temporarily on each item
63381 for (i = 0; i < ln; i++) {
63382 items[i]._current_key = keys[i];
63383 }
63384
63385 // Now we sort our items array
63386 this.handleSort(items);
63387
63388 // And finally we update our keys and indices
63389 for (i = 0; i < ln; i++) {
63390 item = items[i];
63391 key = item._current_key;
63392
63393 keys[i] = key;
63394 indices[key] = i;
63395
63396 delete item._current_key;
63397 }
63398
63399 this.dirtyIndices = true;
63400 },
63401
63402 handleSort: function(items) {
63403 this.mixins.sortable.sort.call(this, items);
63404 },
63405
63406 /**
63407 * Adds an item to the collection.
63408 * @param {String} key
63409 *
63410 * The key to associate with the item, or the new item.
63411 *
63412 * If a {@link #getKey} implementation was specified for this MixedCollection, or if the key of the stored items is
63413 * in a property called **id**, the MixedCollection will be able to _derive_ the key for the new item. In this case
63414 * just pass the new item in this parameter.
63415 * @param {Object} item The item to add.
63416 * @return {Object} The item added.
63417 */
63418 add: function(key, item) {
63419 var me = this,
63420 filtered = this.filtered,
63421 sorted = this.sorted,
63422 all = this.all,
63423 items = this.items,
63424 keys = this.keys,
63425 indices = this.indices,
63426 filterable = this.mixins.filterable,
63427 currentLength = items.length,
63428 index = currentLength;
63429
63430 if (arguments.length == 1) {
63431 item = key;
63432 key = me.getKey(item);
63433 }
63434
63435 if (typeof key != 'undefined' && key !== null) {
63436 if (typeof me.map[key] != 'undefined') {
63437 return me.replace(key, item);
63438 }
63439 me.map[key] = item;
63440 }
63441
63442 all.push(item);
63443
63444 if (filtered && this.getAutoFilter() && filterable.isFiltered.call(me, item)) {
63445 return null;
63446 }
63447
63448 me.length++;
63449
63450 if (sorted && this.getAutoSort()) {
63451 index = this.findInsertionIndex(items, item);
63452 }
63453
63454 if (index !== currentLength) {
63455 this.dirtyIndices = true;
63456
63457 Ext.Array.splice(keys, index, 0, key);
63458 Ext.Array.splice(items, index, 0, item);
63459 } else {
63460 indices[key] = currentLength;
63461
63462 keys.push(key);
63463 items.push(item);
63464 }
63465
63466 return item;
63467 },
63468
63469 /**
63470 * MixedCollection has a generic way to fetch keys if you implement getKey. The default implementation simply
63471 * returns **`item.id`** but you can provide your own implementation to return a different value as in the following
63472 * examples:
63473 *
63474 * // normal way
63475 * var mc = new Ext.util.MixedCollection();
63476 * mc.add(someEl.dom.id, someEl);
63477 * mc.add(otherEl.dom.id, otherEl);
63478 * //and so on
63479 *
63480 * // using getKey
63481 * var mc = new Ext.util.MixedCollection();
63482 * mc.getKey = function(el){
63483 * return el.dom.id;
63484 * };
63485 * mc.add(someEl);
63486 * mc.add(otherEl);
63487 *
63488 * // or via the constructor
63489 * var mc = new Ext.util.MixedCollection(false, function(el){
63490 * return el.dom.id;
63491 * });
63492 * mc.add(someEl);
63493 * mc.add(otherEl);
63494 * @param {Object} item The item for which to find the key.
63495 * @return {Object} The key for the passed item.
63496 */
63497 getKey: function(item) {
63498 return item.id;
63499 },
63500
63501 /**
63502 * Replaces an item in the collection. Fires the {@link #replace} event when complete.
63503 * @param {String} oldKey
63504 *
63505 * The key associated with the item to replace, or the replacement item.
63506 *
63507 * If you supplied a {@link #getKey} implementation for this MixedCollection, or if the key of your stored items is
63508 * in a property called **id**, then the MixedCollection will be able to _derive_ the key of the replacement item.
63509 * If you want to replace an item with one having the same key value, then just pass the replacement item in this
63510 * parameter.
63511 * @param {Object} item {Object} item (optional) If the first parameter passed was a key, the item to associate with
63512 * that key.
63513 * @return {Object} The new item.
63514 */
63515 replace: function(oldKey, item) {
63516 var me = this,
63517 sorted = me.sorted,
63518 filtered = me.filtered,
63519 filterable = me.mixins.filterable,
63520 items = me.items,
63521 keys = me.keys,
63522 all = me.all,
63523 map = me.map,
63524 returnItem = null,
63525 oldItemsLn = items.length,
63526 oldItem, index, newKey;
63527
63528 if (arguments.length == 1) {
63529 item = oldKey;
63530 oldKey = newKey = me.getKey(item);
63531 } else {
63532 newKey = me.getKey(item);
63533 }
63534
63535 oldItem = map[oldKey];
63536 if (typeof oldKey == 'undefined' || oldKey === null || typeof oldItem == 'undefined') {
63537 return me.add(newKey, item);
63538 }
63539
63540 me.map[newKey] = item;
63541 if (newKey !== oldKey) {
63542 delete me.map[oldKey];
63543 }
63544
63545 if (sorted && me.getAutoSort()) {
63546 Ext.Array.remove(items, oldItem);
63547 Ext.Array.remove(keys, oldKey);
63548 Ext.Array.remove(all, oldItem);
63549
63550 all.push(item);
63551
63552 me.dirtyIndices = true;
63553
63554 if (filtered && me.getAutoFilter()) {
63555 // If the item is now filtered we check if it was not filtered
63556 // before. If that is the case then we subtract from the length
63557 if (filterable.isFiltered.call(me, item)) {
63558 if (oldItemsLn !== items.length) {
63559 me.length--;
63560 }
63561 return null;
63562 }
63563 // If the item was filtered, but now it is not anymore then we
63564 // add to the length
63565 else if (oldItemsLn === items.length) {
63566 me.length++;
63567 returnItem = item;
63568 }
63569 }
63570
63571 index = this.findInsertionIndex(items, item);
63572
63573 Ext.Array.splice(keys, index, 0, newKey);
63574 Ext.Array.splice(items, index, 0, item);
63575 } else {
63576 if (filtered) {
63577 if (me.getAutoFilter() && filterable.isFiltered.call(me, item)) {
63578 if (me.indexOf(oldItem) !== -1) {
63579 Ext.Array.remove(items, oldItem);
63580 Ext.Array.remove(keys, oldKey);
63581 me.length--;
63582 me.dirtyIndices = true;
63583 }
63584 return null;
63585 }
63586 else if (me.indexOf(oldItem) === -1) {
63587 items.push(item);
63588 keys.push(newKey);
63589 me.indices[newKey] = me.length;
63590 me.length++;
63591 return item;
63592 }
63593 }
63594
63595 index = me.indexOf(oldItem);
63596
63597 keys[index] = newKey;
63598 items[index] = item;
63599
63600 if (newKey !== oldKey) {
63601 this.dirtyIndices = true;
63602 }
63603 }
63604
63605 return returnItem;
63606 },
63607
63608 /**
63609 * Adds all elements of an Array or an Object to the collection.
63610 * @param {Object/Array} addItems An Object containing properties which will be added to the collection, or an Array of
63611 * values, each of which are added to the collection. Functions references will be added to the collection if {@link}
63612 * Ext.util.MixedCollection#allowFunctions allowFunctions} has been set to `true`.
63613 */
63614 addAll: function(addItems) {
63615 var me = this,
63616 filtered = me.filtered,
63617 sorted = me.sorted,
63618 all = me.all,
63619 items = me.items,
63620 keys = me.keys,
63621 map = me.map,
63622 autoFilter = me.getAutoFilter(),
63623 autoSort = me.getAutoSort(),
63624 newKeys = [],
63625 newItems = [],
63626 filterable = me.mixins.filterable,
63627 addedItems = [],
63628 ln, key, i, item;
63629
63630 if (Ext.isObject(addItems)) {
63631 for (key in addItems) {
63632 if (addItems.hasOwnProperty(key)) {
63633 newItems.push(items[key]);
63634 newKeys.push(key);
63635 }
63636 }
63637 } else {
63638 newItems = addItems;
63639 ln = addItems.length;
63640 for (i = 0; i < ln; i++) {
63641 newKeys.push(me.getKey(addItems[i]));
63642 }
63643 }
63644
63645 for (i = 0; i < ln; i++) {
63646 key = newKeys[i];
63647 item = newItems[i];
63648
63649 if (typeof key != 'undefined' && key !== null) {
63650 if (typeof map[key] != 'undefined') {
63651 me.replace(key, item);
63652 continue;
63653 }
63654 map[key] = item;
63655 }
63656
63657 all.push(item);
63658
63659 if (filtered && autoFilter && filterable.isFiltered.call(me, item)) {
63660 continue;
63661 }
63662
63663 me.length++;
63664
63665 keys.push(key);
63666 items.push(item);
63667
63668 addedItems.push(item);
63669 }
63670
63671 if (addedItems.length) {
63672 me.dirtyIndices = true;
63673
63674 if (sorted && autoSort) {
63675 me.sort();
63676 }
63677
63678 return addedItems;
63679 }
63680
63681 return null;
63682 },
63683
63684 /**
63685 * Executes the specified function once for every item in the collection.
63686 * The function should return a Boolean value. Returning `false` from the function will stop the iteration.
63687 * @param {Function} fn The function to execute for each item.
63688 * @param {Mixed} fn.item The collection item.
63689 * @param {Number} fn.index The item's index.
63690 * @param {Number} fn.length The total number of items in the collection.
63691 * @param {Object} scope The scope (`this` reference) in which the function is executed. Defaults to the current
63692 * item in the iteration.
63693 */
63694 each: function(fn, scope) {
63695 var items = this.items.slice(), // each safe for removal
63696 i = 0,
63697 len = items.length,
63698 item;
63699
63700 for (; i < len; i++) {
63701 item = items[i];
63702 if (fn.call(scope || item, item, i, len) === false) {
63703 break;
63704 }
63705 }
63706 },
63707
63708 /**
63709 * Executes the specified function once for every key in the collection, passing each key, and its associated item
63710 * as the first two parameters.
63711 * @param {Function} fn The function to execute for each item.
63712 * @param {Object} scope The scope (`this` reference) in which the function is executed. Defaults to the browser
63713 * window.
63714 */
63715 eachKey: function(fn, scope) {
63716 var keys = this.keys,
63717 items = this.items,
63718 ln = keys.length, i;
63719
63720 for (i = 0; i < ln; i++) {
63721 fn.call(scope || window, keys[i], items[i], i, ln);
63722 }
63723 },
63724
63725 /**
63726 * Returns the first item in the collection which elicits a `true` return value from the passed selection function.
63727 * @param {Function} fn The selection function to execute for each item.
63728 * @param {Object} scope The scope (`this` reference) in which the function is executed. Defaults to the browser
63729 * window.
63730 * @return {Object} The first item in the collection which returned `true` from the selection function.
63731 */
63732 findBy: function(fn, scope) {
63733 var keys = this.keys,
63734 items = this.items,
63735 i = 0,
63736 len = items.length;
63737
63738 for (; i < len; i++) {
63739 if (fn.call(scope || window, items[i], keys[i])) {
63740 return items[i];
63741 }
63742 }
63743 return null;
63744 },
63745
63746 /**
63747 * Filter by a function. Returns a _new_ collection that has been filtered. The passed function will be called with
63748 * each object in the collection. If the function returns `true`, the value is included otherwise it is filtered.
63749 * @param {Function} fn The function to be called.
63750 * @param {Object} fn.o The object.
63751 * @param {String} fn.k The key.
63752 * @param {Object} scope The scope (`this` reference) in which the function is executed. Defaults to this
63753 * MixedCollection.
63754 * @return {Ext.util.MixedCollection} The new filtered collection
63755 */
63756 filterBy: function(fn, scope) {
63757 var me = this,
63758 newCollection = new this.self(),
63759 keys = me.keys,
63760 items = me.all,
63761 length = items.length,
63762 i;
63763
63764 newCollection.getKey = me.getKey;
63765
63766 for (i = 0; i < length; i++) {
63767 if (fn.call(scope || me, items[i], me.getKey(items[i]))) {
63768 newCollection.add(keys[i], items[i]);
63769 }
63770 }
63771
63772 return newCollection;
63773 },
63774
63775 /**
63776 * Inserts an item at the specified index in the collection. Fires the {@link #add} event when complete.
63777 * @param {Number} index The index to insert the item at.
63778 * @param {String} key The key to associate with the new item, or the item itself.
63779 * @param {Object} item If the second parameter was a key, the new item.
63780 * @return {Object} The item inserted.
63781 */
63782 insert: function(index, key, item) {
63783 var me = this,
63784 sorted = this.sorted,
63785 map = this.map,
63786 filtered = this.filtered;
63787
63788 if (arguments.length == 2) {
63789 item = key;
63790 key = me.getKey(item);
63791 }
63792
63793 if (index >= me.length || (sorted && me.getAutoSort())) {
63794 return me.add(key, item);
63795 }
63796
63797 if (typeof key != 'undefined' && key !== null) {
63798 if (typeof map[key] != 'undefined') {
63799 me.replace(key, item);
63800 return false;
63801 }
63802 map[key] = item;
63803 }
63804
63805 this.all.push(item);
63806
63807 if (filtered && this.getAutoFilter() && this.mixins.filterable.isFiltered.call(me, item)) {
63808 return null;
63809 }
63810
63811 me.length++;
63812
63813 Ext.Array.splice(me.items, index, 0, item);
63814 Ext.Array.splice(me.keys, index, 0, key);
63815
63816 me.dirtyIndices = true;
63817
63818 return item;
63819 },
63820
63821 insertAll: function(index, insertItems) {
63822 if (index >= this.items.length || (this.sorted && this.getAutoSort())) {
63823 return this.addAll(insertItems);
63824 }
63825
63826 var me = this,
63827 filtered = this.filtered,
63828 sorted = this.sorted,
63829 all = this.all,
63830 items = this.items,
63831 keys = this.keys,
63832 map = this.map,
63833 autoFilter = this.getAutoFilter(),
63834 autoSort = this.getAutoSort(),
63835 newKeys = [],
63836 newItems = [],
63837 addedItems = [],
63838 filterable = this.mixins.filterable,
63839 insertedUnfilteredItem = false,
63840 ln, key, i, item;
63841
63842 if (sorted && this.getAutoSort()) {
63843 // <debug>
63844 Ext.Logger.error('Inserting a collection of items into a sorted Collection is invalid. Please just add these items or remove the sorters.');
63845 // </debug>
63846 }
63847
63848 if (Ext.isObject(insertItems)) {
63849 for (key in insertItems) {
63850 if (insertItems.hasOwnProperty(key)) {
63851 newItems.push(items[key]);
63852 newKeys.push(key);
63853 }
63854 }
63855 } else {
63856 newItems = insertItems;
63857 ln = insertItems.length;
63858 for (i = 0; i < ln; i++) {
63859 newKeys.push(me.getKey(insertItems[i]));
63860 }
63861 }
63862
63863 for (i = 0; i < ln; i++) {
63864 key = newKeys[i];
63865 item = newItems[i];
63866
63867 if (typeof key != 'undefined' && key !== null) {
63868 if (typeof map[key] != 'undefined') {
63869 me.replace(key, item);
63870 continue;
63871 }
63872 map[key] = item;
63873 }
63874
63875 all.push(item);
63876
63877 if (filtered && autoFilter && filterable.isFiltered.call(me, item)) {
63878 continue;
63879 }
63880
63881 me.length++;
63882
63883 Ext.Array.splice(items, index + i, 0, item);
63884 Ext.Array.splice(keys, index + i, 0, key);
63885
63886 insertedUnfilteredItem = true;
63887 addedItems.push(item);
63888 }
63889
63890 if (insertedUnfilteredItem) {
63891 this.dirtyIndices = true;
63892
63893 if (sorted && autoSort) {
63894 this.sort();
63895 }
63896
63897 return addedItems;
63898 }
63899
63900 return null;
63901 },
63902
63903 /**
63904 * Remove an item from the collection.
63905 * @param {Object} item The item to remove.
63906 * @return {Object} The item removed or `false` if no item was removed.
63907 */
63908 remove: function(item) {
63909 var index = this.items.indexOf(item);
63910 if (index === -1) {
63911 Ext.Array.remove(this.all, item);
63912
63913 if (typeof this.getKey == 'function') {
63914 var key = this.getKey(item);
63915 if (key !== undefined) {
63916 delete this.map[key];
63917 }
63918 }
63919
63920 return item;
63921 }
63922 return this.removeAt(this.items.indexOf(item));
63923 },
63924
63925 /**
63926 * Remove all items in the passed array from the collection.
63927 * @param {Array} items An array of items to be removed.
63928 * @return {Ext.util.MixedCollection} this object
63929 */
63930 removeAll: function(items) {
63931 if (items) {
63932 var ln = items.length, i;
63933
63934 for (i = 0; i < ln; i++) {
63935 this.remove(items[i]);
63936 }
63937 }
63938
63939 return this;
63940 },
63941
63942 /**
63943 * Remove an item from a specified index in the collection. Fires the {@link #remove} event when complete.
63944 * @param {Number} index The index within the collection of the item to remove.
63945 * @return {Object} The item removed or `false` if no item was removed.
63946 */
63947 removeAt: function(index) {
63948 var me = this,
63949 items = me.items,
63950 keys = me.keys,
63951 all = me.all,
63952 item, key;
63953
63954 if (index < me.length && index >= 0) {
63955 item = items[index];
63956 key = keys[index];
63957
63958 if (typeof key != 'undefined') {
63959 delete me.map[key];
63960 }
63961
63962 Ext.Array.erase(items, index, 1);
63963 Ext.Array.erase(keys, index, 1);
63964 Ext.Array.remove(all, item);
63965
63966 delete me.indices[key];
63967
63968 me.length--;
63969
63970 this.dirtyIndices = true;
63971
63972 return item;
63973 }
63974
63975 return false;
63976 },
63977
63978 /**
63979 * Removed an item associated with the passed key from the collection.
63980 * @param {String} key The key of the item to remove.
63981 * @return {Object/Boolean} The item removed or `false` if no item was removed.
63982 */
63983 removeAtKey: function(key) {
63984 return this.removeAt(this.indexOfKey(key));
63985 },
63986
63987 /**
63988 * Returns the number of items in the collection.
63989 * @return {Number} the number of items in the collection.
63990 */
63991 getCount: function() {
63992 return this.length;
63993 },
63994
63995 /**
63996 * Returns index within the collection of the passed Object.
63997 * @param {Object} item The item to find the index of.
63998 * @return {Number} Index of the item. Returns -1 if not found.
63999 */
64000 indexOf: function(item) {
64001 if (this.dirtyIndices) {
64002 this.updateIndices();
64003 }
64004
64005 var index = item ? this.indices[this.getKey(item)] : -1;
64006 return (index === undefined) ? -1 : index;
64007 },
64008
64009 /**
64010 * Returns index within the collection of the passed key.
64011 * @param {String} key The key to find the index of.
64012 * @return {Number} Index of the key.
64013 */
64014 indexOfKey: function(key) {
64015 if (this.dirtyIndices) {
64016 this.updateIndices();
64017 }
64018
64019 var index = this.indices[key];
64020 return (index === undefined) ? -1 : index;
64021 },
64022
64023 updateIndices: function() {
64024 var items = this.items,
64025 ln = items.length,
64026 indices = this.indices = {},
64027 i, item, key;
64028
64029 for (i = 0; i < ln; i++) {
64030 item = items[i];
64031 key = this.getKey(item);
64032 indices[key] = i;
64033 }
64034
64035 this.dirtyIndices = false;
64036 },
64037
64038 /**
64039 * Returns the item associated with the passed key OR index. Key has priority over index. This is the equivalent of
64040 * calling {@link #getByKey} first, then if nothing matched calling {@link #getAt}.
64041 * @param {String/Number} key The key or index of the item.
64042 * @return {Object} If the item is found, returns the item. If the item was not found, returns `undefined`. If an item
64043 * was found, but is a Class, returns `null`.
64044 */
64045 get: function(key) {
64046 var me = this,
64047 fromMap = me.map[key],
64048 item;
64049
64050 if (fromMap !== undefined) {
64051 item = fromMap;
64052 }
64053 else if (typeof key == 'number') {
64054 item = me.items[key];
64055 }
64056
64057 return typeof item != 'function' || me.getAllowFunctions() ? item : null; // for prototype!
64058 },
64059
64060 /**
64061 * Returns the item at the specified index.
64062 * @param {Number} index The index of the item.
64063 * @return {Object} The item at the specified index.
64064 */
64065 getAt: function(index) {
64066 return this.items[index];
64067 },
64068
64069 /**
64070 * Returns the item associated with the passed key.
64071 * @param {String/Number} key The key of the item.
64072 * @return {Object} The item associated with the passed key.
64073 */
64074 getByKey: function(key) {
64075 return this.map[key];
64076 },
64077
64078 /**
64079 * Returns `true` if the collection contains the passed Object as an item.
64080 * @param {Object} item The Object to look for in the collection.
64081 * @return {Boolean} `true` if the collection contains the Object as an item.
64082 */
64083 contains: function(item) {
64084 var key = this.getKey(item);
64085 if (key) {
64086 return this.containsKey(key);
64087 } else {
64088 return Ext.Array.contains(this.items, item);
64089 }
64090 },
64091
64092 /**
64093 * Returns `true` if the collection contains the passed Object as a key.
64094 * @param {String} key The key to look for in the collection.
64095 * @return {Boolean} `true` if the collection contains the Object as a key.
64096 */
64097 containsKey: function(key) {
64098 return typeof this.map[key] != 'undefined';
64099 },
64100
64101 /**
64102 * Removes all items from the collection. Fires the {@link #clear} event when complete.
64103 */
64104 clear: function(){
64105 var me = this;
64106
64107 me.length = 0;
64108 me.items.length = 0;
64109 me.keys.length = 0;
64110 me.all.length = 0;
64111 me.dirtyIndices = true;
64112 me.indices = {};
64113 me.map = {};
64114 },
64115
64116 /**
64117 * Returns the first item in the collection.
64118 * @return {Object} the first item in the collection.
64119 */
64120 first: function() {
64121 return this.items[0];
64122 },
64123
64124 /**
64125 * Returns the last item in the collection.
64126 * @return {Object} the last item in the collection.
64127 */
64128 last: function() {
64129 return this.items[this.length - 1];
64130 },
64131
64132 /**
64133 * Returns a range of items in this collection
64134 * @param {Number} [start=0] The starting index.
64135 * @param {Number} [end=-1] The ending index. Defaults to the last item.
64136 * @return {Array} An array of items.
64137 */
64138 getRange: function(start, end) {
64139 var me = this,
64140 items = me.items,
64141 range = [],
64142 i;
64143
64144 if (items.length < 1) {
64145 return range;
64146 }
64147
64148 start = start || 0;
64149 end = Math.min(typeof end == 'undefined' ? me.length - 1 : end, me.length - 1);
64150 if (start <= end) {
64151 for (i = start; i <= end; i++) {
64152 range[range.length] = items[i];
64153 }
64154 } else {
64155 for (i = start; i >= end; i--) {
64156 range[range.length] = items[i];
64157 }
64158 }
64159
64160 return range;
64161 },
64162
64163 /**
64164 * Find the index of the first matching object in this collection by a function. If the function returns `true` it
64165 * is considered a match.
64166 * @param {Function} fn The function to be called.
64167 * @param {Object} fn.o The object.
64168 * @param {String} fn.k The key.
64169 * @param {Object} scope The scope (`this` reference) in which the function is executed. Defaults to this
64170 * MixedCollection.
64171 * @param {Number} [start=0] The index to start searching at.
64172 * @return {Number} The matched index, or -1 if the item was not found.
64173 */
64174 findIndexBy: function(fn, scope, start) {
64175 var me = this,
64176 keys = me.keys,
64177 items = me.items,
64178 i = start || 0,
64179 ln = items.length;
64180
64181 for (; i < ln; i++) {
64182 if (fn.call(scope || me, items[i], keys[i])) {
64183 return i;
64184 }
64185 }
64186
64187 return -1;
64188 },
64189
64190 /**
64191 * Creates a shallow copy of this collection
64192 * @return {Ext.util.MixedCollection}
64193 */
64194 clone: function() {
64195 var me = this,
64196 copy = new this.self(),
64197 keys = me.keys,
64198 items = me.items,
64199 i = 0,
64200 ln = items.length;
64201
64202 for(; i < ln; i++) {
64203 copy.add(keys[i], items[i]);
64204 }
64205
64206 copy.getKey = me.getKey;
64207 return copy;
64208 },
64209
64210 destroy: function() {
64211 this.callSuper();
64212 this.clear();
64213 }
64214 });
64215
64216 /**
64217 * @author Ed Spencer
64218 *
64219 * Simple wrapper class that represents a set of records returned by a Proxy.
64220 */
64221 Ext.define('Ext.data.ResultSet', {
64222 config: {
64223 /**
64224 * @cfg {Boolean} loaded
64225 * True if the records have already been loaded. This is only meaningful when dealing with
64226 * SQL-backed proxies.
64227 */
64228 loaded: true,
64229
64230 /**
64231 * @cfg {Number} count
64232 * The number of records in this ResultSet. Note that total may differ from this number.
64233 */
64234 count: null,
64235
64236 /**
64237 * @cfg {Number} total
64238 * The total number of records reported by the data source. This ResultSet may form a subset of
64239 * those records (see {@link #count}).
64240 */
64241 total: null,
64242
64243 /**
64244 * @cfg {Boolean} success
64245 * True if the ResultSet loaded successfully, false if any errors were encountered.
64246 */
64247 success: false,
64248
64249 /**
64250 * @cfg {Ext.data.Model[]} records (required)
64251 * The array of record instances.
64252 */
64253 records: null,
64254
64255 /**
64256 * @cfg {String} message
64257 * The message that was read in from the data
64258 */
64259 message: null
64260 },
64261
64262 /**
64263 * Creates the resultSet
64264 * @param {Object} [config] Config object.
64265 */
64266 constructor: function(config) {
64267 this.initConfig(config);
64268 },
64269
64270 applyCount: function(count) {
64271 if (!count && count !== 0) {
64272 return this.getRecords().length;
64273 }
64274 return count;
64275 },
64276
64277 /**
64278 * @private
64279 * Make sure we set the right count when new records have been sent in
64280 */
64281 updateRecords: function(records) {
64282 this.setCount(records.length);
64283 }
64284 });
64285
64286 /**
64287 * @author Ed Spencer
64288 *
64289 * Readers are used to interpret data to be loaded into a {@link Ext.data.Model Model} instance or a {@link
64290 * Ext.data.Store Store} - often in response to an AJAX request. In general there is usually no need to create
64291 * a Reader instance directly, since a Reader is almost always used together with a {@link Ext.data.proxy.Proxy Proxy},
64292 * and is configured using the Proxy's {@link Ext.data.proxy.Proxy#cfg-reader reader} configuration property:
64293 *
64294 * Ext.define("User", {
64295 * extend: "Ext.data.Model",
64296 * config: {
64297 * fields: [
64298 * "id",
64299 * "name"
64300 * ]
64301 * }
64302 * });
64303 *
64304 * Ext.create("Ext.data.Store", {
64305 * model: "User",
64306 * autoLoad: true,
64307 * storeId: "usersStore",
64308 * proxy: {
64309 * type: "ajax",
64310 * url : "users.json",
64311 * reader: {
64312 * type: "json",
64313 * rootProperty: "users"
64314 * }
64315 * }
64316 * });
64317 *
64318 * Ext.create("Ext.List", {
64319 * fullscreen: true,
64320 * itemTpl: "{name} (id: '{id}')",
64321 * store: "usersStore"
64322 * });
64323 *
64324 * The above reader is configured to consume a JSON string that looks something like this:
64325 *
64326 * {
64327 * "success": true,
64328 * "users": [
64329 * { "name": "User 1" },
64330 * { "name": "User 2" }
64331 * ]
64332 * }
64333 *
64334 *
64335 * # Loading Nested Data
64336 *
64337 * Readers have the ability to automatically load deeply-nested data objects based on the {@link Ext.data.association.Association
64338 * associations} configured on each Model. Below is an example demonstrating the flexibility of these associations in a
64339 * fictional CRM system which manages a User, their Orders, OrderItems and Products. First we'll define the models:
64340 *
64341 * Ext.define("User", {
64342 * extend: "Ext.data.Model",
64343 * config: {
64344 * fields: [
64345 * "id",
64346 * "name"
64347 * ],
64348 * hasMany: {
64349 * model: "Order",
64350 * name: "orders"
64351 * },
64352 * proxy: {
64353 * type: "rest",
64354 * url : "users.json",
64355 * reader: {
64356 * type: "json",
64357 * rootProperty: "users"
64358 * }
64359 * }
64360 * }
64361 * });
64362 *
64363 * Ext.define("Order", {
64364 * extend: "Ext.data.Model",
64365 * config: {
64366 * fields: [
64367 * "id", "total"
64368 * ],
64369 * hasMany: {
64370 * model: "OrderItem",
64371 * name: "orderItems",
64372 * associationKey: "order_items"
64373 * },
64374 * belongsTo: "User"
64375 * }
64376 * });
64377 *
64378 * Ext.define("OrderItem", {
64379 * extend: "Ext.data.Model",
64380 * config: {
64381 * fields: [
64382 * "id",
64383 * "price",
64384 * "quantity",
64385 * "order_id",
64386 * "product_id"
64387 * ],
64388 * belongsTo: [
64389 * "Order", {
64390 * model: "Product",
64391 * associationKey: "product"
64392 * }
64393 * ]
64394 * }
64395 * });
64396 *
64397 * Ext.define("Product", {
64398 * extend: "Ext.data.Model",
64399 * config: {
64400 * fields: [
64401 * "id",
64402 * "name"
64403 * ]
64404 * },
64405 * hasMany: "OrderItem"
64406 * });
64407 *
64408 * var store = Ext.create('Ext.data.Store', {
64409 * model: "User"
64410 * });
64411 *
64412 * store.load({
64413 * callback: function() {
64414 * var output = [];
64415 *
64416 * // the user that was loaded
64417 * var user = store.first();
64418 *
64419 * output.push("Orders for " + user.get('name') + ":");
64420 *
64421 * // iterate over the Orders for each User
64422 * user.orders().each(function(order) {
64423 * output.push("Order ID: " + order.get('id') + ", which contains items:");
64424 *
64425 * // iterate over the OrderItems for each Order
64426 * order.orderItems().each(function(orderItem) {
64427 * // We know that the Product data is already loaded, so we can use the
64428 * // synchronous getProduct() method. Usually, we would use the
64429 * // asynchronous version (see Ext.data.association.BelongsTo).
64430 * var product = orderItem.getProduct();
64431 * output.push(orderItem.get("quantity") + " orders of " + product.get("name"));
64432 * });
64433 * });
64434 * Ext.Msg.alert('Output:', output.join("<br/>"));
64435 * }
64436 * });
64437 *
64438 * This may be a lot to take in - basically a User has many Orders, each of which is composed of several OrderItems.
64439 * Finally, each OrderItem has a single Product. This allows us to consume data like this (_users.json_):
64440 *
64441 * {
64442 * "users": [
64443 * {
64444 * "id": 123,
64445 * "name": "Ed",
64446 * "orders": [
64447 * {
64448 * "id": 50,
64449 * "total": 100,
64450 * "order_items": [
64451 * {
64452 * "id" : 20,
64453 * "price" : 40,
64454 * "quantity": 2,
64455 * "product" : {
64456 * "id": 1000,
64457 * "name": "MacBook Pro"
64458 * }
64459 * },
64460 * {
64461 * "id" : 21,
64462 * "price" : 20,
64463 * "quantity": 3,
64464 * "product" : {
64465 * "id": 1001,
64466 * "name": "iPhone"
64467 * }
64468 * }
64469 * ]
64470 * }
64471 * ]
64472 * }
64473 * ]
64474 * }
64475 *
64476 * The JSON response is deeply nested - it returns all Users (in this case just 1 for simplicity's sake), all of the
64477 * Orders for each User (again just 1 in this case), all of the OrderItems for each Order (2 order items in this case),
64478 * and finally the Product associated with each OrderItem.
64479 *
64480 * Running the code above results in the following:
64481 *
64482 * Orders for Ed:
64483 * Order ID: 50, which contains items:
64484 * 2 orders of MacBook Pro
64485 * 3 orders of iPhone
64486 */
64487 Ext.define('Ext.data.reader.Reader', {
64488
64489
64490
64491 alternateClassName: ['Ext.data.Reader', 'Ext.data.DataReader'],
64492
64493 mixins: [ Ext.mixin.Observable ],
64494
64495 // @private
64496 isReader: true,
64497
64498 config: {
64499 /**
64500 * @cfg {String} idProperty
64501 * Name of the property within a raw object that contains a record identifier value. Defaults to The id of the
64502 * model. If an `idProperty` is explicitly specified it will override that of the one specified on the model
64503 */
64504 idProperty: undefined,
64505
64506 /**
64507 * @cfg {String} clientIdProperty
64508 * The name of the property with a response that contains the existing client side id for a record that we are reading.
64509 */
64510 clientIdProperty: 'clientId',
64511
64512 /**
64513 * @cfg {String} totalProperty
64514 * Name of the property from which to retrieve the total number of records in the dataset. This is only needed if
64515 * the whole dataset is not passed in one go, but is being paged from the remote server.
64516 */
64517 totalProperty: 'total',
64518
64519 /**
64520 * @cfg {String} successProperty
64521 * Name of the property from which to retrieve the success attribute. See
64522 * {@link Ext.data.proxy.Server}.{@link Ext.data.proxy.Server#exception exception} for additional information.
64523 */
64524 successProperty: 'success',
64525
64526 /**
64527 * @cfg {String} messageProperty (optional)
64528 * The name of the property which contains a response message. This property is optional.
64529 */
64530 messageProperty: null,
64531
64532 /**
64533 * @cfg {String} rootProperty
64534 * The name of the property which contains the Array of row objects. For JSON reader it's dot-separated list
64535 * of property names. For XML reader it's a CSS selector. For array reader it's not applicable.
64536 *
64537 * By default the natural root of the data will be used. The root JSON array, the root XML element, or the array.
64538 *
64539 * The data packet value for this property should be an empty array to clear the data or show no data.
64540 */
64541 rootProperty: '',
64542
64543 /**
64544 * @cfg {Boolean} implicitIncludes
64545 * `true` to automatically parse models nested within other models in a response object. See the
64546 * {@link Ext.data.reader.Reader} intro docs for full explanation.
64547 */
64548 implicitIncludes: true,
64549
64550 model: undefined
64551 },
64552
64553 constructor: function(config) {
64554 this.initConfig(config);
64555 },
64556
64557 /**
64558 * @property {Object} metaData
64559 * The raw meta data that was most recently read, if any. Meta data can include existing
64560 * Reader config options like {@link #idProperty}, {@link #totalProperty}, etc. that get
64561 * automatically applied to the Reader, and those can still be accessed directly from the Reader
64562 * if needed. However, meta data is also often used to pass other custom data to be processed
64563 * by application code. For example, it is common when reconfiguring the data model of a grid to
64564 * also pass a corresponding column model config to be applied to the grid. Any such data will
64565 * not get applied to the Reader directly (it just gets passed through and is ignored by Ext).
64566 * This `metaData` property gives you access to all meta data that was passed, including any such
64567 * custom data ignored by the reader.
64568 *
64569 * This is a read-only property, and it will get replaced each time a new meta data object is
64570 * passed to the reader.
64571 * @readonly
64572 */
64573
64574 fieldCount: 0,
64575
64576 applyModel: function(model) {
64577 if (typeof model == 'string') {
64578 model = Ext.data.ModelManager.getModel(model);
64579
64580 if (!model) {
64581 Ext.Logger.error('Model with name ' + arguments[0] + ' doesnt exist.');
64582 }
64583 }
64584
64585 if (model && !model.prototype.isModel && Ext.isObject(model)) {
64586 model = Ext.data.ModelManager.registerType(model.storeId || model.id || Ext.id(), model);
64587 }
64588
64589 return model;
64590 },
64591
64592 applyIdProperty: function(idProperty) {
64593 if (!idProperty && this.getModel()) {
64594 idProperty = this.getModel().getIdProperty();
64595 }
64596 return idProperty;
64597 },
64598
64599 updateModel: function(model) {
64600 if (model) {
64601 if (!this.getIdProperty()) {
64602 this.setIdProperty(model.getIdProperty());
64603 }
64604 this.buildExtractors();
64605 }
64606 },
64607
64608 createAccessor: Ext.emptyFn,
64609
64610 createFieldAccessExpression: function() {
64611 return 'undefined';
64612 },
64613
64614 /**
64615 * @private
64616 * This builds optimized functions for retrieving record data and meta data from an object.
64617 * Subclasses may need to implement their own getRoot function.
64618 */
64619 buildExtractors: function() {
64620 if (!this.getModel()) {
64621 return;
64622 }
64623
64624 var me = this,
64625 totalProp = me.getTotalProperty(),
64626 successProp = me.getSuccessProperty(),
64627 messageProp = me.getMessageProperty();
64628
64629 //build the extractors for all the meta data
64630 if (totalProp) {
64631 me.getTotal = me.createAccessor(totalProp);
64632 }
64633
64634 if (successProp) {
64635 me.getSuccess = me.createAccessor(successProp);
64636 }
64637
64638 if (messageProp) {
64639 me.getMessage = me.createAccessor(messageProp);
64640 }
64641
64642 me.extractRecordData = me.buildRecordDataExtractor();
64643 },
64644
64645 /**
64646 * @private
64647 * Return a function which will read a raw row object in the format this Reader accepts, and populates
64648 * a record's data object with converted data values.
64649 *
64650 * The returned function must be passed the following parameters:
64651 *
64652 * - `dest` - A record's empty data object into which the new field value properties are injected.
64653 * - `source` - A raw row data object of whatever type this Reader consumes
64654 * - `record - The record which is being populated.
64655 */
64656 buildRecordDataExtractor: function() {
64657 var me = this,
64658 model = me.getModel(),
64659 fields = model.getFields(),
64660 ln = fields.length,
64661 fieldVarName = [],
64662 clientIdProp = me.getModel().getClientIdProperty(),
64663 prefix = '__field',
64664 code = [
64665 'var me = this,\n',
64666 ' fields = me.getModel().getFields(),\n',
64667 ' idProperty = me.getIdProperty(),\n',
64668 ' idPropertyIsFn = (typeof idProperty == "function"),',
64669 ' value,\n',
64670 ' internalId'
64671 ], i, field, varName, fieldName;
64672
64673 fields = fields.items;
64674
64675 for (i = 0; i < ln; i++) {
64676 field = fields[i];
64677 fieldName = field.getName();
64678 if (fieldName === model.getIdProperty()) {
64679 fieldVarName[i] = 'idField';
64680 } else {
64681 fieldVarName[i] = prefix + i;
64682 }
64683 code.push(',\n ', fieldVarName[i], ' = fields.get("', field.getName(), '")');
64684 }
64685
64686 code.push(';\n\n return function(source) {\n var dest = {};\n');
64687
64688 code.push(' if (idPropertyIsFn) {\n');
64689 code.push(' idField.setMapping(idProperty);\n');
64690 code.push(' }\n');
64691
64692 for (i = 0; i < ln; i++) {
64693 field = fields[i];
64694 varName = fieldVarName[i];
64695 fieldName = field.getName();
64696 if (fieldName === model.getIdProperty() && field.getMapping() === null && model.getIdProperty() !== this.getIdProperty()) {
64697 field.setMapping(this.getIdProperty());
64698 }
64699 // createFieldAccessExpression must be implemented in subclasses to extract data from the source object in the correct way.
64700 code.push(' try {\n');
64701 code.push(' value = ', me.createFieldAccessExpression(field, varName, 'source'), ';\n');
64702 code.push(' if (value !== undefined) {\n');
64703 code.push(' dest["' + field.getName() + '"] = value;\n');
64704 code.push(' }\n');
64705 code.push(' } catch(e){}\n');
64706 }
64707
64708 // set the client id as the internalId of the record.
64709 // clientId handles the case where a client side record did not previously exist on the server,
64710 // so the server is passing back a client id that can be used to pair the server side record up with the client record
64711 if (clientIdProp) {
64712 code.push(' internalId = ' + me.createFieldAccessExpression(Ext.create('Ext.data.Field', {name: clientIdProp}), null, 'source') + ';\n');
64713 code.push(' if (internalId !== undefined) {\n');
64714 code.push(' dest["_clientId"] = internalId;\n }\n');
64715 }
64716
64717 code.push(' return dest;\n');
64718 code.push(' };');
64719
64720 // Here we are creating a new Function and invoking it immediately in the scope of this Reader
64721 // It declares several vars capturing the configured context of this Reader, and returns a function
64722 // which, when passed a record data object, a raw data row in the format this Reader is configured to read,
64723 // and the record which is being created, will populate the record's data object from the raw row data.
64724 return Ext.functionFactory(code.join('')).call(me);
64725 },
64726
64727 getFields: function() {
64728 return this.getModel().getFields().items;
64729 },
64730
64731 /**
64732 * @private
64733 * By default this function just returns what is passed to it. It can be overridden in a subclass
64734 * to return something else. See XmlReader for an example.
64735 * @param {Object} data The data object
64736 * @return {Object} The normalized data object
64737 */
64738 getData: function(data) {
64739 return data;
64740 },
64741
64742 /**
64743 * Takes a raw response object (as passed to this.read) and returns the useful data segment of it.
64744 * This must be implemented by each subclass
64745 * @param {Object} response The response object
64746 * @return {Object} The useful data from the response
64747 */
64748 getResponseData: function(response) {
64749 return response;
64750 },
64751
64752 /**
64753 * @private
64754 * This will usually need to be implemented in a subclass. Given a generic data object (the type depends on the type
64755 * of data we are reading), this function should return the object as configured by the Reader's 'rootProperty' meta data config.
64756 * See XmlReader's getRoot implementation for an example. By default the same data object will simply be returned.
64757 * @param {Object} data The data object
64758 * @return {Object} The same data object
64759 */
64760 getRoot: function(data) {
64761 return data;
64762 },
64763
64764 /**
64765 * Reads the given response object. This method normalizes the different types of response object that may be passed
64766 * to it, before handing off the reading of records to the {@link #readRecords} function.
64767 * @param {Object} response The response object. This may be either an XMLHttpRequest object or a plain JS object
64768 * @return {Ext.data.ResultSet} The parsed ResultSet object
64769 */
64770 read: function(response) {
64771 var data = response,
64772 Model = this.getModel(),
64773 resultSet, records, i, ln, record;
64774
64775 if (response) {
64776 data = this.getResponseData(response);
64777 }
64778
64779 if (data) {
64780 resultSet = this.readRecords(data);
64781 records = resultSet.getRecords();
64782 for (i = 0, ln = records.length; i < ln; i++) {
64783 record = records[i];
64784 records[i] = new Model(record.data, record.id, record.node);
64785 }
64786 return resultSet;
64787 } else {
64788 return this.nullResultSet;
64789 }
64790 },
64791
64792 process: function(response) {
64793 var data = response;
64794
64795 if (response) {
64796 data = this.getResponseData(response);
64797 }
64798
64799 if (data) {
64800 return this.readRecords(data);
64801 } else {
64802 return this.nullResultSet;
64803 }
64804 },
64805
64806 /**
64807 * Abstracts common functionality used by all Reader subclasses. Each subclass is expected to call this function
64808 * before running its own logic and returning the Ext.data.ResultSet instance. For most Readers additional
64809 * processing should not be needed.
64810 * @param {Object} data The raw data object
64811 * @return {Ext.data.ResultSet} A ResultSet object
64812 */
64813 readRecords: function(data) {
64814 var me = this;
64815
64816 /**
64817 * @property {Object} rawData
64818 * The raw data object that was last passed to readRecords. Stored for further processing if needed
64819 */
64820 me.rawData = data;
64821
64822 data = me.getData(data);
64823
64824 if (data.metaData) {
64825 me.onMetaChange(data.metaData);
64826 }
64827
64828 // <debug>
64829 if (!me.getModel()) {
64830 Ext.Logger.warn('In order to read record data, a Reader needs to have a Model defined on it.');
64831 }
64832 // </debug>
64833
64834 // If we pass an array as the data, we don't use getRoot on the data.
64835 // Instead the root equals to the data.
64836 var isArray = Ext.isArray(data),
64837 root = isArray ? data : me.getRoot(data),
64838 success = true,
64839 recordCount = 0,
64840 total, value, records, message;
64841
64842 if (isArray && Ext.isEmpty(data.length)) {
64843 return me.nullResultSet;
64844 }
64845
64846 // buildExtractors should have put getTotal, getSuccess, or getMessage methods on the instance.
64847 // So we can check them directly
64848 if (me.getTotal) {
64849 value = parseInt(me.getTotal(data), 10);
64850 if (!isNaN(value)) {
64851 total = value;
64852 }
64853 }
64854
64855 if (me.getSuccess) {
64856 value = me.getSuccess(data);
64857 if (value === false || value === 'false') {
64858 success = false;
64859 }
64860 }
64861
64862 if (me.getMessage) {
64863 message = me.getMessage(data);
64864 }
64865
64866 if (root) {
64867 records = me.extractData(root);
64868 recordCount = records.length;
64869 } else {
64870 recordCount = 0;
64871 records = [];
64872 }
64873
64874 return new Ext.data.ResultSet({
64875 total : total,
64876 count : recordCount,
64877 records: records,
64878 success: success,
64879 message: message
64880 });
64881 },
64882
64883 /**
64884 * Returns extracted, type-cast rows of data.
64885 * @param {Object[]/Object} root from server response
64886 * @private
64887 */
64888 extractData : function(root) {
64889 var me = this,
64890 records = [],
64891 length = root.length,
64892 model = me.getModel(),
64893 idProperty = model.getIdProperty(),
64894 fieldsCollection = model.getFields(),
64895 node, i, data, id, clientId;
64896
64897 /*
64898 * We check here whether the fields are dirty since the last read.
64899 * This works around an issue when a Model is used for both a Tree and another
64900 * source, because the tree decorates the model with extra fields and it causes
64901 * issues because the readers aren't notified.
64902 */
64903 if (fieldsCollection.isDirty) {
64904 me.buildExtractors(true);
64905 delete fieldsCollection.isDirty;
64906 }
64907
64908 if (!root.length && Ext.isObject(root)) {
64909 root = [root];
64910 length = 1;
64911 }
64912
64913 for (i = 0; i < length; i++) {
64914 clientId = null;
64915 id = null;
64916
64917 node = root[i];
64918
64919 // When you use a Memory proxy, and you set data: [] to contain record instances
64920 // this node will already be a record. In this case we should not try to extract
64921 // the record data from the object, but just use the record data attribute.
64922 if (node.isModel) {
64923 data = node.data;
64924 } else {
64925 data = me.extractRecordData(node);
64926 }
64927
64928 if (data._clientId !== undefined) {
64929 clientId = data._clientId;
64930 delete data._clientId;
64931 }
64932
64933 if (data[idProperty] !== undefined) {
64934 id = data[idProperty];
64935 }
64936
64937 if (me.getImplicitIncludes()) {
64938 me.readAssociated(data, node);
64939 }
64940
64941 records.push({
64942 clientId: clientId,
64943 id: id,
64944 data: data,
64945 node: node
64946 });
64947 }
64948
64949 return records;
64950 },
64951
64952 /**
64953 * @private
64954 * Loads a record's associations from the data object. This pre-populates `hasMany` and `belongsTo` associations
64955 * on the record provided.
64956 * @param {Ext.data.Model} record The record to load associations for
64957 * @param {Object} data The data object
64958 */
64959 readAssociated: function(record, data) {
64960 var associations = this.getModel().associations.items,
64961 length = associations.length,
64962 i = 0,
64963 association, associationData, associationKey;
64964
64965 for (; i < length; i++) {
64966 association = associations[i];
64967 associationKey = association.getAssociationKey();
64968 associationData = this.getAssociatedDataRoot(data, associationKey);
64969
64970 if (associationData) {
64971 record[associationKey] = associationData;
64972 }
64973 }
64974 },
64975
64976 /**
64977 * @private
64978 * Used internally by `readAssociated`. Given a data object (which could be json, xml etc) for a specific
64979 * record, this should return the relevant part of that data for the given association name. If a complex
64980 * mapping, this will traverse arrays and objects to resolve the data.
64981 * @param {Object} data The raw data object
64982 * @param {String} associationName The name of the association to get data for (uses associationKey if present)
64983 * @return {Object} The root
64984 */
64985 getAssociatedDataRoot: function(data, associationName) {
64986 var re = /[\[\.]/,
64987 i = String(associationName).search(re);
64988
64989 if (i >= 0) {
64990 return Ext.functionFactory('obj', 'return obj' + (i > 0 ? '.' : '') + associationName)(data);
64991 }
64992
64993 return data[associationName];
64994 },
64995
64996 /**
64997 * @private
64998 * Reconfigures the meta data tied to this Reader
64999 */
65000 onMetaChange : function(meta) {
65001 var fields = meta.fields,
65002 me = this,
65003 newModel, config, idProperty;
65004
65005 // save off the raw meta data
65006 me.metaData = meta;
65007
65008 // set any reader-specific configs from meta if available
65009 if (meta.rootProperty !== undefined) {
65010 me.setRootProperty(meta.rootProperty);
65011 }
65012 else if (meta.root !== undefined) {
65013 me.setRootProperty(meta.root);
65014 }
65015
65016 if (meta.idProperty !== undefined) {
65017 me.setIdProperty(meta.idProperty);
65018 }
65019 if (meta.totalProperty !== undefined) {
65020 me.setTotalProperty(meta.totalProperty);
65021 }
65022 if (meta.successProperty !== undefined) {
65023 me.setSuccessProperty(meta.successProperty);
65024 }
65025 if (meta.messageProperty !== undefined) {
65026 me.setMessageProperty(meta.messageProperty);
65027 }
65028
65029 if (fields) {
65030 if (me.getModel()) {
65031 me.getModel().setFields(fields);
65032 me.buildExtractors();
65033 }
65034 else {
65035 idProperty = me.getIdProperty();
65036 config = {fields: fields};
65037
65038 if (idProperty) {
65039 config.idProperty = idProperty;
65040 }
65041
65042 newModel = Ext.define("Ext.data.reader.MetaModel" + Ext.id(), {
65043 extend: 'Ext.data.Model',
65044 config: config
65045 });
65046
65047 me.setModel(newModel);
65048 }
65049 }
65050 else {
65051 me.buildExtractors();
65052 }
65053 }
65054
65055
65056 // Convert old properties in data into a config object
65057 }, function() {
65058 Ext.apply(this.prototype, {
65059 // @private
65060 // Empty ResultSet to return when response is falsy (null|undefined|empty string)
65061 nullResultSet: new Ext.data.ResultSet({
65062 total : 0,
65063 count : 0,
65064 records: [],
65065 success: false
65066 })
65067 });
65068
65069 });
65070
65071 /**
65072 * The JSON Reader is used by a Proxy to read a server response that is sent back in JSON format. This usually happens
65073 * as a result of loading a Store - for example we might create something like this:
65074 *
65075 * Ext.define('User', {
65076 * extend: 'Ext.data.Model',
65077 * config: {
65078 * fields: ['id', 'name', 'email']
65079 * }
65080 * });
65081 *
65082 * var store = Ext.create('Ext.data.Store', {
65083 * model: 'User',
65084 * proxy: {
65085 * type: 'ajax',
65086 * url : 'users.json',
65087 * reader: {
65088 * type: 'json'
65089 * }
65090 * }
65091 * });
65092 *
65093 * The example above creates a 'User' model. Models are explained in the {@link Ext.data.Model Model} docs if you're not
65094 * already familiar with them.
65095 *
65096 * We created the simplest type of JSON Reader possible by simply telling our {@link Ext.data.Store Store}'s {@link
65097 * Ext.data.proxy.Proxy Proxy} that we want a JSON Reader. The Store automatically passes the configured model to the
65098 * Store, so it is as if we passed this instead:
65099 *
65100 * reader: {
65101 * type : 'json',
65102 * model: 'User'
65103 * }
65104 *
65105 * The reader we set up is ready to read data from our server - at the moment it will accept a response like this:
65106 *
65107 * [
65108 * {
65109 * "id": 1,
65110 * "name": "Ed Spencer",
65111 * "email": "ed@sencha.com"
65112 * },
65113 * {
65114 * "id": 2,
65115 * "name": "Abe Elias",
65116 * "email": "abe@sencha.com"
65117 * }
65118 * ]
65119 *
65120 * ## Reading other JSON formats
65121 *
65122 * If you already have your JSON format defined and it doesn't look quite like what we have above, you can usually pass
65123 * JsonReader a couple of configuration options to make it parse your format. For example, we can use the
65124 * {@link #rootProperty} configuration to parse data that comes back like this:
65125 *
65126 * {
65127 * "users": [
65128 * {
65129 * "id": 1,
65130 * "name": "Ed Spencer",
65131 * "email": "ed@sencha.com"
65132 * },
65133 * {
65134 * "id": 2,
65135 * "name": "Abe Elias",
65136 * "email": "abe@sencha.com"
65137 * }
65138 * ]
65139 * }
65140 *
65141 * To parse this we just pass in a {@link #rootProperty} configuration that matches the 'users' above:
65142 *
65143 * reader: {
65144 * type: 'json',
65145 * rootProperty: 'users'
65146 * }
65147 *
65148 * Sometimes the JSON structure is even more complicated. Document databases like CouchDB often provide metadata around
65149 * each record inside a nested structure like this:
65150 *
65151 * {
65152 * "total": 122,
65153 * "offset": 0,
65154 * "users": [
65155 * {
65156 * "id": "ed-spencer-1",
65157 * "value": 1,
65158 * "user": {
65159 * "id": 1,
65160 * "name": "Ed Spencer",
65161 * "email": "ed@sencha.com"
65162 * }
65163 * }
65164 * ]
65165 * }
65166 *
65167 * In the case above the record data is nested an additional level inside the "users" array as each "user" item has
65168 * additional metadata surrounding it ('id' and 'value' in this case). To parse data out of each "user" item in the JSON
65169 * above we need to specify the {@link #record} configuration like this:
65170 *
65171 * reader: {
65172 * type: 'json',
65173 * record: 'user',
65174 * rootProperty: 'users'
65175 * }
65176 *
65177 * ## Response MetaData
65178 *
65179 * The server can return metadata in its response, in addition to the record data, that describe attributes
65180 * of the data set itself or are used to reconfigure the Reader. To pass metadata in the response you simply
65181 * add a `metaData` attribute to the root of the response data. The metaData attribute can contain anything,
65182 * but supports a specific set of properties that are handled by the Reader if they are present:
65183 *
65184 * - {@link #idProperty}: property name for the primary key field of the data
65185 * - {@link #rootProperty}: the property name of the root response node containing the record data
65186 * - {@link #totalProperty}: property name for the total number of records in the data
65187 * - {@link #successProperty}: property name for the success status of the response
65188 * - {@link #messageProperty}: property name for an optional response message
65189 * - {@link Ext.data.Model#cfg-fields fields}: Config used to reconfigure the Model's fields before converting the
65190 * response data into records
65191 *
65192 * An initial Reader configuration containing all of these properties might look like this ("fields" would be
65193 * included in the Model definition, not shown):
65194 *
65195 * reader: {
65196 * type: 'json',
65197 * idProperty: 'id',
65198 * rootProperty: 'root',
65199 * totalProperty: 'total',
65200 * successProperty: 'success',
65201 * messageProperty: 'message'
65202 * }
65203 *
65204 * If you were to pass a response object containing attributes different from those initially defined above, you could
65205 * use the `metaData` attribute to reconfigure the Reader on the fly. For example:
65206 *
65207 * {
65208 * "count": 1,
65209 * "ok": true,
65210 * "msg": "Users found",
65211 * "users": [{
65212 * "userId": 123,
65213 * "name": "Ed Spencer",
65214 * "email": "ed@sencha.com"
65215 * }],
65216 * "metaData": {
65217 * "idProperty": 'userId',
65218 * "rootProperty": "users",
65219 * "totalProperty": 'count',
65220 * "successProperty": 'ok',
65221 * "messageProperty": 'msg'
65222 * }
65223 * }
65224 *
65225 * You can also place any other arbitrary data you need into the `metaData` attribute which will be ignored by the Reader,
65226 * but will be accessible via the Reader's {@link #metaData} property. Application code can then process the passed
65227 * metadata in any way it chooses.
65228 *
65229 * 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
65230 * the `fields` property the Model will be automatically updated by the Reader internally, but that change will not be
65231 * reflected automatically in the grid unless you also update the column configuration. You could do this manually, or you
65232 * could simply pass a standard grid column config object as part of the `metaData` attribute
65233 * and then pass that along to the grid. Here's a very simple example for how that could be accomplished:
65234 *
65235 * // response format:
65236 * {
65237 * ...
65238 * "metaData": {
65239 * "fields": [
65240 * { "name": "userId", "type": "int" },
65241 * { "name": "name", "type": "string" },
65242 * { "name": "birthday", "type": "date", "dateFormat": "Y-j-m" },
65243 * ],
65244 * "columns": [
65245 * { "text": "User ID", "dataIndex": "userId", "width": 40 },
65246 * { "text": "User Name", "dataIndex": "name", "flex": 1 },
65247 * { "text": "Birthday", "dataIndex": "birthday", "flex": 1, "format": 'Y-j-m', "xtype": "datecolumn" }
65248 * ]
65249 * }
65250 * }
65251 */
65252 Ext.define('Ext.data.reader.Json', {
65253 extend: Ext.data.reader.Reader ,
65254 alternateClassName: 'Ext.data.JsonReader',
65255 alias : 'reader.json',
65256
65257 config: {
65258 /**
65259 * @cfg {String} [record=null]
65260 * The optional location within the JSON response that the record data itself can be found at. See the
65261 * JsonReader intro docs for more details. This is not often needed.
65262 */
65263 record: null,
65264
65265 /**
65266 * @cfg {Boolean} [useSimpleAccessors=false]
65267 * `true` to ensure that field names/mappings are treated as literals when reading values. For
65268 * example, by default, using the mapping "foo.bar.baz" will try and read a property foo from the root, then a
65269 * property bar from foo, then a property baz from bar. Setting the simple accessors to `true` will read the
65270 * property with the name "foo.bar.baz" direct from the root object.
65271 */
65272 useSimpleAccessors: false
65273 },
65274
65275 objectRe: /[\[\.]/,
65276
65277 // @inheritdoc
65278 getResponseData: function(response) {
65279 var responseText = response;
65280
65281 // Handle an XMLHttpRequest object
65282 if (response && response.responseText) {
65283 responseText = response.responseText;
65284 }
65285
65286 // Handle the case where data has already been decoded
65287 if (typeof responseText !== 'string') {
65288 return responseText;
65289 }
65290
65291 var data;
65292 try {
65293 data = Ext.decode(responseText);
65294 }
65295 catch (ex) {
65296 /**
65297 * @event exception Fires whenever the reader is unable to parse a response.
65298 * @param {Ext.data.reader.Xml} reader A reference to this reader.
65299 * @param {XMLHttpRequest} response The XMLHttpRequest response object.
65300 * @param {String} error The error message.
65301 */
65302 this.fireEvent('exception', this, response, 'Unable to parse the JSON returned by the server: ' + ex.toString());
65303 Ext.Logger.warn('Unable to parse the JSON returned by the server: ' + ex.toString());
65304 }
65305 //<debug>
65306 if (!data) {
65307 this.fireEvent('exception', this, response, 'JSON object not found');
65308
65309 Ext.Logger.warn('JSON object not found');
65310 }
65311 //</debug>
65312
65313 return data;
65314 },
65315
65316 // @inheritdoc
65317 buildExtractors: function() {
65318 var me = this,
65319 root = me.getRootProperty();
65320
65321 me.callParent(arguments);
65322
65323 if (root) {
65324 me.rootAccessor = me.createAccessor(root);
65325 } else {
65326 delete me.rootAccessor;
65327 }
65328 },
65329
65330 /**
65331 * We create this method because `root` is now a config so `getRoot` is already defined, but in the old
65332 * data package `getRoot` was passed a data argument and it would return the data inside of the `root`
65333 * property. This method handles both cases.
65334 * @param {Array/Object} [data]
65335 * @return {String/Object} Returns the config root value if this method was called without passing
65336 * data. Else it returns the object in the data bound to the root.
65337 * @private
65338 */
65339 getRoot: function(data) {
65340 var fieldsCollection = this.getModel().getFields();
65341
65342 /*
65343 * We check here whether the fields are dirty since the last read.
65344 * This works around an issue when a Model is used for both a Tree and another
65345 * source, because the tree decorates the model with extra fields and it causes
65346 * issues because the readers aren't notified.
65347 */
65348 if (fieldsCollection.isDirty) {
65349 this.buildExtractors(true);
65350 delete fieldsCollection.isDirty;
65351 }
65352
65353 if (this.rootAccessor) {
65354 return this.rootAccessor.call(this, data);
65355 } else {
65356 return data;
65357 }
65358 },
65359
65360 /**
65361 * @private
65362 * We're just preparing the data for the superclass by pulling out the record objects we want. If a {@link #record}
65363 * was specified we have to pull those out of the larger JSON object, which is most of what this function is doing
65364 * @param {Object} root The JSON root node
65365 * @return {Ext.data.Model[]} The records
65366 */
65367 extractData: function(root) {
65368 var recordName = this.getRecord(),
65369 data = [],
65370 length, i;
65371
65372 if (recordName) {
65373 length = root.length;
65374
65375 if (!length && Ext.isObject(root)) {
65376 length = 1;
65377 root = [root];
65378 }
65379
65380 for (i = 0; i < length; i++) {
65381 data[i] = root[i][recordName];
65382 }
65383 } else {
65384 data = root;
65385 }
65386 return this.callParent([data]);
65387 },
65388
65389 /**
65390 * @private
65391 * Returns an accessor function for the given property string. Gives support for properties such as the following:
65392 * 'someProperty'
65393 * 'some.property'
65394 * 'some["property"]'
65395 * This is used by buildExtractors to create optimized extractor functions when casting raw data into model instances.
65396 */
65397 createAccessor: function() {
65398 var re = /[\[\.]/;
65399
65400 return function(expr) {
65401 if (Ext.isEmpty(expr)) {
65402 return Ext.emptyFn;
65403 }
65404 if (Ext.isFunction(expr)) {
65405 return expr;
65406 }
65407 if (this.getUseSimpleAccessors() !== true) {
65408 var i = String(expr).search(re);
65409 if (i >= 0) {
65410 return Ext.functionFactory('obj', 'var value; try {value = obj' + (i > 0 ? '.' : '') + expr + '} catch(e) {}; return value;');
65411 }
65412 }
65413 return function(obj) {
65414 return obj[expr];
65415 };
65416 };
65417 }(),
65418
65419 /**
65420 * @private
65421 * Returns an accessor expression for the passed Field. Gives support for properties such as the following:
65422 * 'someProperty'
65423 * 'some.property'
65424 * 'some["property"]'
65425 * This is used by buildExtractors to create optimized on extractor function which converts raw data into model instances.
65426 */
65427 createFieldAccessExpression: function(field, fieldVarName, dataName) {
65428 var me = this,
65429 re = me.objectRe,
65430 hasMap = (field.getMapping() !== null),
65431 map = hasMap ? field.getMapping() : field.getName(),
65432 result, operatorSearch;
65433
65434 if (typeof map === 'function') {
65435 result = fieldVarName + '.getMapping()(' + dataName + ', this)';
65436 }
65437 else if (me.getUseSimpleAccessors() === true || ((operatorSearch = String(map).search(re)) < 0)) {
65438 if (!hasMap || isNaN(map)) {
65439 // If we don't provide a mapping, we may have a field name that is numeric
65440 map = '"' + map + '"';
65441 }
65442 result = dataName + "[" + map + "]";
65443 }
65444 else {
65445 result = dataName + (operatorSearch > 0 ? '.' : '') + map;
65446 }
65447
65448 return result;
65449 }
65450 });
65451
65452 /**
65453 * @author Ed Spencer
65454 *
65455 * Base Writer class used by most subclasses of {@link Ext.data.proxy.Server}. This class is
65456 * responsible for taking a set of {@link Ext.data.Operation} objects and a {@link Ext.data.Request}
65457 * object and modifying that request based on the Operations.
65458 *
65459 * For example a Ext.data.writer.Json would format the Operations and their {@link Ext.data.Model}
65460 * instances based on the config options passed to the JsonWriter's constructor.
65461 *
65462 * Writers are not needed for any kind of local storage - whether via a
65463 * {@link Ext.data.proxy.WebStorage Web Storage proxy} (see {@link Ext.data.proxy.LocalStorage localStorage})
65464 * or just in memory via a {@link Ext.data.proxy.Memory MemoryProxy}.
65465 */
65466 Ext.define('Ext.data.writer.Writer', {
65467 alias: 'writer.base',
65468 alternateClassName: ['Ext.data.DataWriter', 'Ext.data.Writer'],
65469
65470 config: {
65471 /**
65472 * @cfg {Boolean} writeAllFields `true` to write all fields from the record to the server. If set to `false` it
65473 * will only send the fields that were modified. Note that any fields that have
65474 * {@link Ext.data.Field#persist} set to false will still be ignored.
65475 */
65476 writeAllFields: true,
65477
65478 /**
65479 * @cfg {String} nameProperty This property is used to read the key for each value that will be sent to the server.
65480 * For example:
65481 *
65482 * Ext.define('Person', {
65483 * extend: 'Ext.data.Model',
65484 * fields: [{
65485 * name: 'first',
65486 * mapping: 'firstName'
65487 * }, {
65488 * name: 'last',
65489 * mapping: 'lastName'
65490 * }, {
65491 * name: 'age'
65492 * }]
65493 * });
65494 *
65495 * new Ext.data.writer.Writer({
65496 * writeAllFields: true,
65497 * nameProperty: 'mapping'
65498 * });
65499 *
65500 * The following data will be sent to the server:
65501 *
65502 * {
65503 * firstName: 'first name value',
65504 * lastName: 'last name value',
65505 * age: 1
65506 * }
65507 *
65508 * If the value is not present, the field name will always be used.
65509 */
65510 nameProperty: 'name'
65511 },
65512
65513 /**
65514 * Creates new Writer.
65515 * @param {Object} config (optional) Config object.
65516 */
65517 constructor: function(config) {
65518 this.initConfig(config);
65519 },
65520
65521 /**
65522 * Prepares a Proxy's Ext.data.Request object.
65523 * @param {Ext.data.Request} request The request object.
65524 * @return {Ext.data.Request} The modified request object.
65525 */
65526 write: function(request) {
65527 var operation = request.getOperation(),
65528 records = operation.getRecords() || [],
65529 len = records.length,
65530 i = 0,
65531 data = [];
65532
65533 for (; i < len; i++) {
65534 data.push(this.getRecordData(records[i]));
65535 }
65536 return this.writeRecords(request, data);
65537 },
65538
65539 writeDate: function(field, date) {
65540 if (!date) {
65541 return null;
65542 }
65543
65544 var dateFormat = field.getDateFormat() || 'timestamp';
65545 switch (dateFormat) {
65546 case 'timestamp':
65547 return date.getTime()/1000;
65548 case 'time':
65549 return date.getTime();
65550 default:
65551 return Ext.Date.format(date, dateFormat);
65552 }
65553 },
65554
65555 /**
65556 * Formats the data for each record before sending it to the server. This
65557 * method should be overridden to format the data in a way that differs from the default.
65558 * @param {Object} record The record that we are writing to the server.
65559 * @return {Object} An object literal of name/value keys to be written to the server.
65560 * By default this method returns the data property on the record.
65561 */
65562 getRecordData: function(record) {
65563 var isPhantom = record.phantom === true,
65564 writeAll = this.getWriteAllFields() || isPhantom,
65565 nameProperty = this.getNameProperty(),
65566 fields = record.getFields(),
65567 data = {},
65568 changes, name, field, key, value;
65569
65570 if (writeAll) {
65571 fields.each(function(field) {
65572 if (field.getPersist()) {
65573 name = field.config[nameProperty] || field.getName();
65574 value = record.get(field.getName());
65575 if (field.getType().type == 'date') {
65576 value = this.writeDate(field, value);
65577 }
65578 data[name] = value;
65579 }
65580 }, this);
65581 } else {
65582 // Only write the changes
65583 changes = record.getChanges();
65584 for (key in changes) {
65585 if (changes.hasOwnProperty(key)) {
65586 field = fields.get(key);
65587 if (field.getPersist()) {
65588 name = field.config[nameProperty] || field.getName();
65589 value = changes[key];
65590 if (field.getType().type == 'date') {
65591 value = this.writeDate(field, value);
65592 }
65593 data[name] = value;
65594 }
65595 }
65596 }
65597 if (!isPhantom) {
65598 // always include the id for non phantoms
65599 data[record.getIdProperty()] = record.getId();
65600 }
65601 }
65602 return data;
65603 }
65604
65605 // Convert old properties in data into a config object
65606 });
65607
65608 /**
65609 * This class is used to write {@link Ext.data.Model} data to the server in a JSON format.
65610 * The {@link #allowSingle} configuration can be set to false to force the records to always be
65611 * encoded in an array, even if there is only a single record being sent.
65612 */
65613 Ext.define('Ext.data.writer.Json', {
65614 extend: Ext.data.writer.Writer ,
65615 alternateClassName: 'Ext.data.JsonWriter',
65616 alias: 'writer.json',
65617
65618 config: {
65619 /**
65620 * @cfg {String} rootProperty
65621 * The key under which the records in this Writer will be placed. If you specify {@link #encode} to be true,
65622 * we default this to 'records'.
65623 *
65624 * Example generated request, using root: 'records':
65625 *
65626 * {'records': [{name: 'my record'}, {name: 'another record'}]}
65627 *
65628 */
65629 rootProperty: undefined,
65630
65631 /**
65632 * @cfg {Boolean} encode
65633 * True to use Ext.encode() on the data before sending. The encode option should only be set to true when a
65634 * {@link #root} is defined, because the values will be sent as part of the request parameters as opposed to
65635 * a raw post. The root will be the name of the parameter sent to the server.
65636 */
65637 encode: false,
65638
65639 /**
65640 * @cfg {Boolean} allowSingle
65641 * False to ensure that records are always wrapped in an array, even if there is only one record being sent.
65642 * When there is more than one record, they will always be encoded into an array.
65643 *
65644 * Example:
65645 *
65646 * // with allowSingle: true
65647 * "root": {
65648 * "first": "Mark",
65649 * "last": "Corrigan"
65650 * }
65651 *
65652 * // with allowSingle: false
65653 * "root": [{
65654 * "first": "Mark",
65655 * "last": "Corrigan"
65656 * }]
65657 */
65658 allowSingle: true,
65659
65660 encodeRequest: false
65661 },
65662
65663 applyRootProperty: function(root) {
65664 if (!root && (this.getEncode() || this.getEncodeRequest())) {
65665 root = 'data';
65666 }
65667 return root;
65668 },
65669
65670 //inherit docs
65671 writeRecords: function(request, data) {
65672 var root = this.getRootProperty(),
65673 params = request.getParams(),
65674 allowSingle = this.getAllowSingle(),
65675 jsonData;
65676
65677 if (this.getAllowSingle() && data && data.length == 1) {
65678 // convert to single object format
65679 data = data[0];
65680 }
65681
65682 if (this.getEncodeRequest()) {
65683 jsonData = request.getJsonData() || {};
65684 if (data && (data.length || (allowSingle && Ext.isObject(data)))) {
65685 jsonData[root] = data;
65686 }
65687 request.setJsonData(Ext.apply(jsonData, params || {}));
65688 request.setParams(null);
65689 request.setMethod('POST');
65690 return request;
65691 }
65692
65693 if (!data || !(data.length || (allowSingle && Ext.isObject(data)))) {
65694 return request;
65695 }
65696
65697 if (this.getEncode()) {
65698 if (root) {
65699 // sending as a param, need to encode
65700 params[root] = Ext.encode(data);
65701 } else {
65702 //<debug>
65703 Ext.Logger.error('Must specify a root when using encode');
65704 //</debug>
65705 }
65706 } else {
65707 // send as jsonData
65708 jsonData = request.getJsonData() || {};
65709 if (root) {
65710 jsonData[root] = data;
65711 } else {
65712 jsonData = data;
65713 }
65714 request.setJsonData(jsonData);
65715 }
65716 return request;
65717 }
65718
65719 });
65720
65721
65722 /*
65723 * @allowSingle: true
65724 * @encodeRequest: false
65725 * Url: update.json?param1=test
65726 * {'field1': 'test': 'field2': 'test'}
65727 *
65728 * @allowSingle: false
65729 * @encodeRequest: false
65730 * Url: update.json?param1=test
65731 * [{'field1': 'test', 'field2': 'test'}]
65732 *
65733 * @allowSingle: true
65734 * @root: 'data'
65735 * @encodeRequest: true
65736 * Url: update.json
65737 * {
65738 * 'param1': 'test',
65739 * 'data': {'field1': 'test', 'field2': 'test'}
65740 * }
65741 *
65742 * @allowSingle: false
65743 * @root: 'data'
65744 * @encodeRequest: true
65745 * Url: update.json
65746 * {
65747 * 'param1': 'test',
65748 * 'data': [{'field1': 'test', 'field2': 'test'}]
65749 * }
65750 *
65751 * @allowSingle: true
65752 * @root: data
65753 * @encodeRequest: false
65754 * Url: update.json
65755 * param1=test&data={'field1': 'test', 'field2': 'test'}
65756 *
65757 * @allowSingle: false
65758 * @root: data
65759 * @encodeRequest: false
65760 * @ncode: true
65761 * Url: update.json
65762 * param1=test&data=[{'field1': 'test', 'field2': 'test'}]
65763 *
65764 * @allowSingle: true
65765 * @root: data
65766 * @encodeRequest: false
65767 * Url: update.json?param1=test&data={'field1': 'test', 'field2': 'test'}
65768 *
65769 * @allowSingle: false
65770 * @root: data
65771 * @encodeRequest: false
65772 * Url: update.json?param1=test&data=[{'field1': 'test', 'field2': 'test'}]
65773 */
65774
65775 /**
65776 * @author Ed Spencer
65777 * @class Ext.data.Batch
65778 *
65779 * Provides a mechanism to run one or more {@link Ext.data.Operation operations} in a given order. Fires the `operationcomplete` event
65780 * after the completion of each Operation, and the `complete` event when all Operations have been successfully executed. Fires an `exception`
65781 * event if any of the Operations encounter an exception.
65782 *
65783 * Usually these are only used internally by {@link Ext.data.proxy.Proxy} classes.
65784 */
65785 Ext.define('Ext.data.Batch', {
65786 mixins: {
65787 observable: Ext.mixin.Observable
65788 },
65789
65790 config: {
65791 /**
65792 * @cfg {Boolean} autoStart `true` to immediately start processing the batch as soon as it is constructed.
65793 */
65794 autoStart: false,
65795
65796 /**
65797 * @cfg {Boolean} pauseOnException `true` to automatically pause the execution of the batch if any operation encounters an exception.
65798 */
65799 pauseOnException: true,
65800
65801 /**
65802 * @cfg {Ext.data.Proxy} proxy The proxy this Batch belongs to. Used to make the requests for each operation in the Batch.
65803 */
65804 proxy: null
65805 },
65806
65807 /**
65808 * The index of the current operation being executed.
65809 * @property current
65810 * @type Number
65811 */
65812 current: -1,
65813
65814 /**
65815 * The total number of operations in this batch.
65816 * @property total
65817 * @type Number
65818 * @readonly
65819 */
65820 total: 0,
65821
65822 /**
65823 * `true` if the batch is currently running.
65824 * @property isRunning
65825 * @type Boolean
65826 */
65827 isRunning: false,
65828
65829 /**
65830 * `true` if this batch has been executed completely.
65831 * @property isComplete
65832 * @type Boolean
65833 */
65834 isComplete: false,
65835
65836 /**
65837 * `true` if this batch has encountered an exception. This is cleared at the start of each operation.
65838 * @property hasException
65839 * @type Boolean
65840 */
65841 hasException: false,
65842
65843 /**
65844 * @event complete
65845 * Fired when all operations of this batch have been completed.
65846 * @param {Ext.data.Batch} batch The batch object.
65847 * @param {Object} operation The last operation that was executed.
65848 */
65849
65850 /**
65851 * @event exception
65852 * Fired when a operation encountered an exception.
65853 * @param {Ext.data.Batch} batch The batch object.
65854 * @param {Object} operation The operation that encountered the exception.
65855 */
65856
65857 /**
65858 * @event operationcomplete
65859 * Fired when each operation of the batch completes.
65860 * @param {Ext.data.Batch} batch The batch object.
65861 * @param {Object} operation The operation that just completed.
65862 */
65863
65864 /**
65865 * Creates new Batch object.
65866 * @param {Object} config (optional) Config object.
65867 */
65868 constructor: function(config) {
65869 var me = this;
65870
65871 me.initConfig(config);
65872
65873 /**
65874 * Ordered array of operations that will be executed by this batch
65875 * @property {Ext.data.Operation[]} operations
65876 */
65877 me.operations = [];
65878 },
65879
65880 /**
65881 * Adds a new operation to this batch.
65882 * @param {Object} operation The {@link Ext.data.Operation Operation} object.
65883 */
65884 add: function(operation) {
65885 this.total++;
65886
65887 operation.setBatch(this);
65888
65889 this.operations.push(operation);
65890 },
65891
65892 /**
65893 * Kicks off the execution of the batch, continuing from the next operation if the previous
65894 * operation encountered an exception, or if execution was paused.
65895 */
65896 start: function() {
65897 this.hasException = false;
65898 this.isRunning = true;
65899
65900 this.runNextOperation();
65901 },
65902
65903 /**
65904 * @private
65905 * Runs the next operation, relative to `this.current`.
65906 */
65907 runNextOperation: function() {
65908 this.runOperation(this.current + 1);
65909 },
65910
65911 /**
65912 * Pauses execution of the batch, but does not cancel the current operation.
65913 */
65914 pause: function() {
65915 this.isRunning = false;
65916 },
65917
65918 /**
65919 * Executes a operation by its numeric index.
65920 * @param {Number} index The operation index to run.
65921 */
65922 runOperation: function(index) {
65923 var me = this,
65924 operations = me.operations,
65925 operation = operations[index],
65926 onProxyReturn;
65927
65928 if (operation === undefined) {
65929 me.isRunning = false;
65930 me.isComplete = true;
65931 me.fireEvent('complete', me, operations[operations.length - 1]);
65932 } else {
65933 me.current = index;
65934
65935 onProxyReturn = function(operation) {
65936 var hasException = operation.hasException();
65937
65938 if (hasException) {
65939 me.hasException = true;
65940 me.fireEvent('exception', me, operation);
65941 } else {
65942 me.fireEvent('operationcomplete', me, operation);
65943 }
65944
65945 if (hasException && me.getPauseOnException()) {
65946 me.pause();
65947 } else {
65948 operation.setCompleted();
65949 me.runNextOperation();
65950 }
65951 };
65952
65953 operation.setStarted();
65954
65955
65956 me.getProxy()[operation.getAction()](operation, onProxyReturn, me);
65957 }
65958 }
65959 });
65960
65961 /**
65962 * @author Ed Spencer
65963 *
65964 * Represents a single read or write operation performed by a {@link Ext.data.proxy.Proxy Proxy}. Operation objects are
65965 * used to enable communication between Stores and Proxies. Application developers should rarely need to interact with
65966 * Operation objects directly.
65967 *
65968 * Note that when you define an Operation directly, you need to specify at least the {@link #model} configuration.
65969 *
65970 * Several Operations can be batched together in a {@link Ext.data.Batch batch}.
65971 */
65972 Ext.define('Ext.data.Operation', {
65973 config: {
65974 /**
65975 * @cfg {Boolean} synchronous
65976 * True if this Operation is to be executed synchronously. This property is inspected by a
65977 * {@link Ext.data.Batch Batch} to see if a series of Operations can be executed in parallel or not.
65978 * @accessor
65979 */
65980 synchronous: true,
65981
65982 /**
65983 * @cfg {String} action
65984 * The action being performed by this Operation. Should be one of 'create', 'read', 'update' or 'destroy'.
65985 * @accessor
65986 */
65987 action: null,
65988
65989 /**
65990 * @cfg {Ext.util.Filter[]} filters
65991 * Optional array of filter objects. Only applies to 'read' actions.
65992 * @accessor
65993 */
65994 filters: null,
65995
65996 /**
65997 * @cfg {Ext.util.Sorter[]} sorters
65998 * Optional array of sorter objects. Only applies to 'read' actions.
65999 * @accessor
66000 */
66001 sorters: null,
66002
66003 /**
66004 * @cfg {Ext.util.Grouper} grouper
66005 * Optional grouping configuration. Only applies to 'read' actions where grouping is desired.
66006 * @accessor
66007 */
66008 grouper: null,
66009
66010 /**
66011 * @cfg {Number} start
66012 * The start index (offset), used in paging when running a 'read' action.
66013 * @accessor
66014 */
66015 start: null,
66016
66017 /**
66018 * @cfg {Number} limit
66019 * The number of records to load. Used on 'read' actions when paging is being used.
66020 * @accessor
66021 */
66022 limit: null,
66023
66024 /**
66025 * @cfg {Ext.data.Batch} batch
66026 * The batch that this Operation is a part of.
66027 * @accessor
66028 */
66029 batch: null,
66030
66031 /**
66032 * @cfg {Function} callback
66033 * Function to execute when operation completed.
66034 * @cfg {Ext.data.Model[]} callback.records Array of records.
66035 * @cfg {Ext.data.Operation} callback.operation The Operation itself.
66036 * @accessor
66037 */
66038 callback: null,
66039
66040 /**
66041 * @cfg {Object} scope
66042 * Scope for the {@link #callback} function.
66043 * @accessor
66044 */
66045 scope: null,
66046
66047 /**
66048 * @cfg {Ext.data.ResultSet} resultSet
66049 * The ResultSet for this operation.
66050 * @accessor
66051 */
66052 resultSet: null,
66053
66054 /**
66055 * @cfg {Array} records
66056 * The records associated to this operation. Before an operation starts, these
66057 * are the records you are updating, creating, or destroying. After an operation
66058 * is completed, a Proxy usually sets these records on the Operation to become
66059 * the processed records. If you don't set these records on your operation in
66060 * your proxy, then the getter will return the ones defined on the {@link #resultSet}
66061 * @accessor
66062 */
66063 records: null,
66064
66065 /**
66066 * @cfg {Ext.data.Request} request
66067 * The request used for this Operation. Operations don't usually care about Request and Response data, but in the
66068 * ServerProxy and any of its subclasses they may be useful for further processing.
66069 * @accessor
66070 */
66071 request: null,
66072
66073 /**
66074 * @cfg {Object} response
66075 * The response that was gotten from the server if there was one.
66076 * @accessor
66077 */
66078 response: null,
66079
66080 /**
66081 * @cfg {Boolean} withCredentials
66082 * This field is necessary when using cross-origin resource sharing.
66083 * @accessor
66084 */
66085 withCredentials: null,
66086
66087 /**
66088 * @cfg {Object} params
66089 * The params send along with this operation. These usually apply to a Server proxy if you are
66090 * creating your own custom proxy,
66091 * @accessor
66092 */
66093 params: null,
66094 url: null,
66095 page: null,
66096 node: null,
66097
66098 /**
66099 * @cfg {Ext.data.Model} model
66100 * The Model that this Operation will be dealing with. This configuration is required when defining any Operation.
66101 * Since Operations take care of creating, updating, destroying and reading records, it needs access to the Model.
66102 * @accessor
66103 */
66104 model: undefined,
66105
66106 addRecords: false
66107 },
66108
66109 /**
66110 * @property {Boolean} started
66111 * Property tracking the start status of this Operation. Use {@link #isStarted}.
66112 * @private
66113 * @readonly
66114 */
66115 started: false,
66116
66117 /**
66118 * @property {Boolean} running
66119 * Property tracking the run status of this Operation. Use {@link #isRunning}.
66120 * @private
66121 * @readonly
66122 */
66123 running: false,
66124
66125 /**
66126 * @property {Boolean} complete
66127 * Property tracking the completion status of this Operation. Use {@link #isComplete}.
66128 * @private
66129 * @readonly
66130 */
66131 complete: false,
66132
66133 /**
66134 * @property {Boolean} success
66135 * Property tracking whether the Operation was successful or not. This starts as undefined and is set to `true`
66136 * or `false` by the Proxy that is executing the Operation. It is also set to false by {@link #setException}. Use
66137 * {@link #wasSuccessful} to query success status.
66138 * @private
66139 * @readonly
66140 */
66141 success: undefined,
66142
66143 /**
66144 * @property {Boolean} exception
66145 * Property tracking the exception status of this Operation. Use {@link #hasException} and see {@link #getError}.
66146 * @private
66147 * @readonly
66148 */
66149 exception: false,
66150
66151 /**
66152 * @property {String/Object} error
66153 * The error object passed when {@link #setException} was called. This could be any object or primitive.
66154 * @private
66155 */
66156 error: undefined,
66157
66158 /**
66159 * Creates new Operation object.
66160 * @param {Object} config (optional) Config object.
66161 */
66162 constructor: function(config) {
66163 this.initConfig(config);
66164 },
66165
66166 applyModel: function(model) {
66167 if (typeof model == 'string') {
66168 model = Ext.data.ModelManager.getModel(model);
66169
66170 if (!model) {
66171 Ext.Logger.error('Model with name ' + arguments[0] + ' doesnt exist.');
66172 }
66173 }
66174
66175 if (model && !model.prototype.isModel && Ext.isObject(model)) {
66176 model = Ext.data.ModelManager.registerType(model.storeId || model.id || Ext.id(), model);
66177 }
66178
66179 // <debug>
66180 if (!model) {
66181 Ext.Logger.warn('Unless you define your model using metadata, an Operation needs to have a model defined.');
66182 }
66183 // </debug>
66184
66185 return model;
66186 },
66187
66188 getRecords: function() {
66189 var resultSet = this.getResultSet();
66190 return this._records || (resultSet ? resultSet.getRecords() : []);
66191 },
66192
66193 /**
66194 * Marks the Operation as started.
66195 */
66196 setStarted: function() {
66197 this.started = true;
66198 this.running = true;
66199 },
66200
66201 /**
66202 * Marks the Operation as completed.
66203 */
66204 setCompleted: function() {
66205 this.complete = true;
66206 this.running = false;
66207 },
66208
66209 /**
66210 * Marks the Operation as successful.
66211 */
66212 setSuccessful: function() {
66213 this.success = true;
66214 },
66215
66216 /**
66217 * Marks the Operation as having experienced an exception. Can be supplied with an option error message/object.
66218 * @param {String/Object} error (optional) error string/object
66219 */
66220 setException: function(error) {
66221 this.exception = true;
66222 this.success = false;
66223 this.running = false;
66224 this.error = error;
66225 },
66226
66227 /**
66228 * Returns `true` if this Operation encountered an exception (see also {@link #getError}).
66229 * @return {Boolean} `true` if there was an exception.
66230 */
66231 hasException: function() {
66232 return this.exception === true;
66233 },
66234
66235 /**
66236 * Returns the error string or object that was set using {@link #setException}.
66237 * @return {String/Object} The error object.
66238 */
66239 getError: function() {
66240 return this.error;
66241 },
66242
66243 /**
66244 * Returns `true` if the Operation has been started. Note that the Operation may have started AND completed, see
66245 * {@link #isRunning} to test if the Operation is currently running.
66246 * @return {Boolean} `true` if the Operation has started
66247 */
66248 isStarted: function() {
66249 return this.started === true;
66250 },
66251
66252 /**
66253 * Returns `true` if the Operation has been started but has not yet completed.
66254 * @return {Boolean} `true` if the Operation is currently running
66255 */
66256 isRunning: function() {
66257 return this.running === true;
66258 },
66259
66260 /**
66261 * Returns `true` if the Operation has been completed
66262 * @return {Boolean} `true` if the Operation is complete
66263 */
66264 isComplete: function() {
66265 return this.complete === true;
66266 },
66267
66268 /**
66269 * Returns `true` if the Operation has completed and was successful
66270 * @return {Boolean} `true` if successful
66271 */
66272 wasSuccessful: function() {
66273 return this.isComplete() && this.success === true;
66274 },
66275
66276 /**
66277 * Checks whether this operation should cause writing to occur.
66278 * @return {Boolean} Whether the operation should cause a write to occur.
66279 */
66280 allowWrite: function() {
66281 return this.getAction() != 'read';
66282 },
66283
66284 process: function(action, resultSet, request, response) {
66285 if (resultSet.getSuccess() !== false) {
66286 this.setResponse(response);
66287 this.setResultSet(resultSet);
66288 this.setCompleted();
66289 this.setSuccessful();
66290 } else {
66291 this.setResponse(response);
66292 this.setResultSet(resultSet);
66293 return false;
66294 }
66295
66296 return this['process' + Ext.String.capitalize(action)].call(this, resultSet, request, response);
66297 },
66298
66299 processRead: function(resultSet) {
66300 var records = resultSet.getRecords(),
66301 processedRecords = [],
66302 Model = this.getModel(),
66303 ln = records.length,
66304 i, record;
66305
66306 for (i = 0; i < ln; i++) {
66307 record = records[i];
66308 processedRecords.push(new Model(record.data, record.id, record.node));
66309 }
66310
66311 this.setRecords(processedRecords);
66312 resultSet.setRecords(processedRecords);
66313 return true;
66314 },
66315
66316 processCreate: function(resultSet) {
66317 var updatedRecords = resultSet.getRecords(),
66318 currentRecords = this.getRecords(),
66319 ln = updatedRecords.length,
66320 i, currentRecord, updatedRecord;
66321
66322 for (i = 0; i < ln; i++) {
66323 updatedRecord = updatedRecords[i];
66324
66325 if (updatedRecord.clientId === null && currentRecords.length == 1 && updatedRecords.length == 1) {
66326 currentRecord = currentRecords[i];
66327 } else {
66328 currentRecord = this.findCurrentRecord(updatedRecord.clientId);
66329 }
66330
66331 if (currentRecord) {
66332 this.updateRecord(currentRecord, updatedRecord);
66333 }
66334 // <debug>
66335 else {
66336 Ext.Logger.warn('Unable to match the record that came back from the server.');
66337 }
66338 // </debug>
66339 }
66340
66341 return true;
66342 },
66343
66344 processUpdate: function(resultSet) {
66345 var updatedRecords = resultSet.getRecords(),
66346 currentRecords = this.getRecords(),
66347 ln = updatedRecords.length,
66348 i, currentRecord, updatedRecord;
66349
66350 for (i = 0; i < ln; i++) {
66351 updatedRecord = updatedRecords[i];
66352 currentRecord = currentRecords[i];
66353
66354 if (currentRecord) {
66355 this.updateRecord(currentRecord, updatedRecord);
66356 }
66357 // <debug>
66358 else {
66359 Ext.Logger.warn('Unable to match the updated record that came back from the server.');
66360 }
66361 // </debug>
66362 }
66363
66364 return true;
66365 },
66366
66367 processDestroy: function(resultSet) {
66368 var updatedRecords = resultSet.getRecords(),
66369 ln = updatedRecords.length,
66370 i, currentRecord, updatedRecord;
66371
66372 for (i = 0; i < ln; i++) {
66373 updatedRecord = updatedRecords[i];
66374 currentRecord = this.findCurrentRecord(updatedRecord.id);
66375
66376 if (currentRecord) {
66377 currentRecord.setIsErased(true);
66378 currentRecord.notifyStores('afterErase', currentRecord);
66379 }
66380 // <debug>
66381 else {
66382 Ext.Logger.warn('Unable to match the destroyed record that came back from the server.');
66383 }
66384 // </debug>
66385 }
66386 },
66387
66388 findCurrentRecord: function(clientId) {
66389 var currentRecords = this.getRecords(),
66390 ln = currentRecords.length,
66391 i, currentRecord;
66392
66393 for (i = 0; i < ln; i++) {
66394 currentRecord = currentRecords[i];
66395 if (currentRecord.getId() === clientId) {
66396 return currentRecord;
66397 }
66398 }
66399 },
66400
66401 updateRecord: function(currentRecord, updatedRecord) {
66402 var recordData = updatedRecord.data,
66403 recordId = updatedRecord.id;
66404
66405 currentRecord.beginEdit();
66406
66407 currentRecord.set(recordData);
66408 if (recordId !== null) {
66409 currentRecord.setId(recordId);
66410 }
66411
66412 // We call endEdit with silent: true because the commit below already makes
66413 // sure any store is notified of the record being updated.
66414 currentRecord.endEdit(true);
66415
66416 currentRecord.commit();
66417 }
66418 });
66419
66420 /**
66421 * @author Ed Spencer
66422 *
66423 * Proxies are used by {@link Ext.data.Store Stores} to handle the loading and saving of {@link Ext.data.Model Model}
66424 * data. Usually developers will not need to create or interact with proxies directly.
66425 *
66426 * # Types of Proxy
66427 *
66428 * There are two main types of Proxy - {@link Ext.data.proxy.Client Client} and {@link Ext.data.proxy.Server Server}.
66429 * The Client proxies save their data locally and include the following subclasses:
66430 *
66431 * - {@link Ext.data.proxy.LocalStorage LocalStorageProxy} - saves its data to localStorage if the browser supports it
66432 * - {@link Ext.data.proxy.Memory MemoryProxy} - holds data in memory only, any data is lost when the page is refreshed
66433 *
66434 * The Server proxies save their data by sending requests to some remote server. These proxies include:
66435 *
66436 * - {@link Ext.data.proxy.Ajax Ajax} - sends requests to a server on the same domain
66437 * - {@link Ext.data.proxy.JsonP JsonP} - uses JSON-P to send requests to a server on a different domain
66438 *
66439 * Proxies operate on the principle that all operations performed are either Create, Read, Update or Delete. These four
66440 * operations are mapped to the methods {@link #create}, {@link #read}, {@link #update} and {@link #destroy}
66441 * respectively. Each Proxy subclass implements these functions.
66442 *
66443 * The CRUD methods each expect an {@link Ext.data.Operation Operation} object as the sole argument. The Operation
66444 * encapsulates information about the action the Store wishes to perform, the {@link Ext.data.Model model} instances
66445 * that are to be modified, etc. See the {@link Ext.data.Operation Operation} documentation for more details. Each CRUD
66446 * method also accepts a callback function to be called asynchronously on completion.
66447 *
66448 * Proxies also support batching of Operations via a {@link Ext.data.Batch batch} object, invoked by the {@link #batch}
66449 * method.
66450 *
66451 * ###Further Reading
66452 * [Sencha Touch Data Overview](../../../core_concepts/data/data_package_overview.html)
66453 * [Sencha Touch Store Guide](../../../core_concepts/data/stores.html)
66454 * [Sencha Touch Models Guide](../../../core_concepts/data/models.html)
66455 * [Sencha Touch Proxy Guide](../../../core_concepts/data/proxies.html)
66456 */
66457 Ext.define('Ext.data.proxy.Proxy', {
66458 extend: Ext.Evented ,
66459
66460 alias: 'proxy.proxy',
66461
66462 alternateClassName: ['Ext.data.DataProxy', 'Ext.data.Proxy'],
66463
66464
66465
66466
66467
66468
66469
66470
66471 config: {
66472 /**
66473 * @cfg {String} batchOrder
66474 * Comma-separated ordering 'create', 'update' and 'destroy' actions when batching. Override this to set a different
66475 * order for the batched CRUD actions to be executed in.
66476 * @accessor
66477 */
66478 batchOrder: 'create,update,destroy',
66479
66480 /**
66481 * @cfg {Boolean} batchActions
66482 * True to batch actions of a particular type when synchronizing the store.
66483 * @accessor
66484 */
66485 batchActions: true,
66486
66487 /**
66488 * @cfg {String/Ext.data.Model} model (required)
66489 * The name of the Model to tie to this Proxy. Can be either the string name of the Model, or a reference to the
66490 * Model constructor.
66491 * @accessor
66492 */
66493 model: null,
66494
66495 /**
66496 * @cfg {Object/String/Ext.data.reader.Reader} reader
66497 * The Ext.data.reader.Reader to use to decode the server's response or data read from client. This can either be a
66498 * Reader instance, a config object or just a valid Reader type name (e.g. 'json', 'xml').
66499 * @accessor
66500 */
66501 reader: {
66502 type: 'json'
66503 },
66504
66505 /**
66506 * @cfg {Object/String/Ext.data.writer.Writer} writer
66507 * The Ext.data.writer.Writer to use to encode any request sent to the server or saved to client. This can either be
66508 * a Writer instance, a config object or just a valid Writer type name (e.g. 'json', 'xml').
66509 * @accessor
66510 */
66511 writer: {
66512 type: 'json'
66513 }
66514 },
66515
66516 isProxy: true,
66517
66518 applyModel: function(model) {
66519 if (typeof model == 'string') {
66520 model = Ext.data.ModelManager.getModel(model);
66521
66522 if (!model) {
66523 Ext.Logger.error('Model with name ' + arguments[0] + ' doesnt exist.');
66524 }
66525 }
66526
66527 if (model && !model.prototype.isModel && Ext.isObject(model)) {
66528 model = Ext.data.ModelManager.registerType(model.storeId || model.id || Ext.id(), model);
66529 }
66530
66531 return model;
66532 },
66533
66534 updateModel: function(model) {
66535 if (model) {
66536 var reader = this.getReader();
66537 if (reader && !reader.getModel()) {
66538 reader.setModel(model);
66539 }
66540 }
66541 },
66542
66543 applyReader: function(reader, currentReader) {
66544 return Ext.factory(reader, Ext.data.Reader, currentReader, 'reader');
66545 },
66546
66547 updateReader: function(reader) {
66548 if (reader) {
66549 var model = this.getModel();
66550 if (!model) {
66551 model = reader.getModel();
66552 if (model) {
66553 this.setModel(model);
66554 }
66555 } else {
66556 reader.setModel(model);
66557 }
66558
66559 if (reader.onMetaChange) {
66560 reader.onMetaChange = Ext.Function.createSequence(reader.onMetaChange, this.onMetaChange, this);
66561 }
66562 }
66563 },
66564
66565 onMetaChange: function(data) {
66566 var model = this.getReader().getModel();
66567 if (!this.getModel() && model) {
66568 this.setModel(model);
66569 }
66570
66571 /**
66572 * @event metachange
66573 * Fires whenever the server has sent back new metadata to reconfigure the Reader.
66574 * @param {Ext.data.Proxy} this
66575 * @param {Object} data The metadata sent back from the server
66576 */
66577 this.fireEvent('metachange', this, data);
66578 },
66579
66580 applyWriter: function(writer, currentWriter) {
66581 return Ext.factory(writer, Ext.data.Writer, currentWriter, 'writer');
66582 },
66583
66584 /**
66585 * Performs the given create operation. If you override this method in a custom Proxy, remember to always call the provided
66586 * callback method when you are done with your operation.
66587 * @param {Ext.data.Operation} operation The Operation to perform
66588 * @param {Function} callback Callback function to be called when the Operation has completed (whether successful or not)
66589 * @param {Object} scope Scope to execute the callback function in
66590 * @method
66591 */
66592 create: Ext.emptyFn,
66593
66594 /**
66595 * Performs the given read operation. If you override this method in a custom Proxy, remember to always call the provided
66596 * callback method when you are done with your operation.
66597 * @param {Ext.data.Operation} operation The Operation to perform
66598 * @param {Function} callback Callback function to be called when the Operation has completed (whether successful or not)
66599 * @param {Object} scope Scope to execute the callback function in
66600 * @method
66601 */
66602 read: Ext.emptyFn,
66603
66604 /**
66605 * Performs the given update operation. If you override this method in a custom Proxy, remember to always call the provided
66606 * callback method when you are done with your operation.
66607 * @param {Ext.data.Operation} operation The Operation to perform
66608 * @param {Function} callback Callback function to be called when the Operation has completed (whether successful or not)
66609 * @param {Object} scope Scope to execute the callback function in
66610 * @method
66611 */
66612 update: Ext.emptyFn,
66613
66614 /**
66615 * Performs the given destroy operation. If you override this method in a custom Proxy, remember to always call the provided
66616 * callback method when you are done with your operation.
66617 * @param {Ext.data.Operation} operation The Operation to perform
66618 * @param {Function} callback Callback function to be called when the Operation has completed (whether successful or not)
66619 * @param {Object} scope Scope to execute the callback function in
66620 * @method
66621 */
66622 destroy: Ext.emptyFn,
66623
66624 onDestroy: function() {
66625 Ext.destroy(this.getReader(), this.getWriter());
66626 Ext.Evented.prototype.destroy.apply(this, arguments);
66627 },
66628
66629 /**
66630 * Performs a batch of {@link Ext.data.Operation Operations}, in the order specified by {@link #batchOrder}. Used
66631 * internally by {@link Ext.data.Store}'s {@link Ext.data.Store#sync sync} method. Example usage:
66632 *
66633 * myProxy.batch({
66634 * create : [myModel1, myModel2],
66635 * update : [myModel3],
66636 * destroy: [myModel4, myModel5]
66637 * });
66638 *
66639 * Where the myModel* above are {@link Ext.data.Model Model} instances - in this case 1 and 2 are new instances and
66640 * have not been saved before, 3 has been saved previously but needs to be updated, and 4 and 5 have already been
66641 * saved but should now be destroyed.
66642 *
66643 * @param {Object} options Object containing one or more properties supported by the batch method:
66644 *
66645 * @param {Object} options.operations Object containing the Model instances to act upon, keyed by action name
66646 *
66647 * @param {Object} [options.listeners] Event listeners object passed straight through to the Batch -
66648 * see {@link Ext.data.Batch} for details
66649 *
66650 * @param {Ext.data.Batch/Object} [options.batch] A {@link Ext.data.Batch} object (or batch config to apply
66651 * to the created batch). If unspecified a default batch will be auto-created.
66652 *
66653 * @param {Function} [options.callback] The function to be called upon completion of processing the batch.
66654 * The callback is called regardless of success or failure and is passed the following parameters:
66655 * @param {Ext.data.Batch} options.callback.batch The {@link Ext.data.Batch batch} that was processed,
66656 * containing all operations in their current state after processing
66657 * @param {Object} options.callback.options The options argument that was originally passed into batch
66658 *
66659 * @param {Function} [options.success] The function to be called upon successful completion of the batch. The
66660 * success function is called only if no exceptions were reported in any operations. If one or more exceptions
66661 * occurred then the `failure` function will be called instead. The success function is called
66662 * with the following parameters:
66663 * @param {Ext.data.Batch} options.success.batch The {@link Ext.data.Batch batch} that was processed,
66664 * containing all operations in their current state after processing
66665 * @param {Object} options.success.options The options argument that was originally passed into batch
66666 *
66667 * @param {Function} [options.failure] The function to be called upon unsuccessful completion of the batch. The
66668 * failure function is called when one or more operations returns an exception during processing (even if some
66669 * operations were also successful). The failure function is called with the following parameters:
66670 * @param {Ext.data.Batch} options.failure.batch The {@link Ext.data.Batch batch} that was processed,
66671 * containing all operations in their current state after processing
66672 * @param {Object} options.failure.options The options argument that was originally passed into batch
66673 *
66674 * @param {Object} [options.scope] The scope in which to execute any callbacks (i.e. the `this` object inside
66675 * the callback, success and/or failure functions). Defaults to the proxy.
66676 *
66677 * @return {Ext.data.Batch} The newly created Batch
66678 */
66679 batch: function(options, /* deprecated */listeners) {
66680 var me = this,
66681 useBatch = me.getBatchActions(),
66682 model = me.getModel(),
66683 batch,
66684 records;
66685
66686 if (options.operations === undefined) {
66687 // the old-style (operations, listeners) signature was called
66688 // so convert to the single options argument syntax
66689 options = {
66690 operations: options,
66691 listeners: listeners
66692 };
66693
66694 // <debug warn>
66695 Ext.Logger.deprecate('Passes old-style signature to Proxy.batch (operations, listeners). Please convert to single options argument syntax.');
66696 // </debug>
66697 }
66698
66699 if (options.batch && options.batch.isBatch) {
66700 batch = options.batch;
66701 } else {
66702 batch = new Ext.data.Batch(options.batch || {});
66703 }
66704
66705 batch.setProxy(me);
66706
66707 batch.on('complete', Ext.bind(me.onBatchComplete, me, [options], 0));
66708 if (options.listeners) {
66709 batch.on(options.listeners);
66710 }
66711
66712 Ext.each(me.getBatchOrder().split(','), function(action) {
66713 records = options.operations[action];
66714 if (records) {
66715 if (useBatch) {
66716 batch.add(new Ext.data.Operation({
66717 action: action,
66718 records: records,
66719 model: model
66720 }));
66721 } else {
66722 Ext.each(records, function(record) {
66723 batch.add(new Ext.data.Operation({
66724 action : action,
66725 records: [record],
66726 model: model
66727 }));
66728 });
66729 }
66730 }
66731 }, me);
66732
66733 batch.start();
66734 return batch;
66735 },
66736
66737 /**
66738 * @private
66739 * The internal callback that the proxy uses to call any specified user callbacks after completion of a batch
66740 */
66741 onBatchComplete: function(batchOptions, batch) {
66742 var scope = batchOptions.scope || this;
66743
66744 if (batch.hasException) {
66745 if (Ext.isFunction(batchOptions.failure)) {
66746 Ext.callback(batchOptions.failure, scope, [batch, batchOptions]);
66747 }
66748 } else if (Ext.isFunction(batchOptions.success)) {
66749 Ext.callback(batchOptions.success, scope, [batch, batchOptions]);
66750 }
66751
66752 if (Ext.isFunction(batchOptions.callback)) {
66753 Ext.callback(batchOptions.callback, scope, [batch, batchOptions]);
66754 }
66755
66756 Ext.destroy(batch);
66757 }
66758
66759 }, function() {
66760 // Ext.data2.proxy.ProxyMgr.registerType('proxy', this);
66761
66762 //backwards compatibility
66763 // Ext.data.DataProxy = this;
66764 // Ext.deprecate('platform', '2.0', function() {
66765 // Ext.data2.DataProxy = this;
66766 // }, this);
66767 });
66768
66769 /**
66770 * @author Ed Spencer
66771 *
66772 * Base class for any client-side storage. Used as a superclass for {@link Ext.data.proxy.Memory Memory} and
66773 * {@link Ext.data.proxy.WebStorage Web Storage} proxies. Do not use directly, use one of the subclasses instead.
66774 * @private
66775 */
66776 Ext.define('Ext.data.proxy.Client', {
66777 extend: Ext.data.proxy.Proxy ,
66778 alternateClassName: 'Ext.proxy.ClientProxy',
66779
66780 /**
66781 * Abstract function that must be implemented by each ClientProxy subclass. This should purge all record data
66782 * from the client side storage, as well as removing any supporting data (such as lists of record IDs)
66783 */
66784 clear: function() {
66785 //<debug>
66786 Ext.Logger.error("The Ext.data.proxy.Client subclass that you are using has not defined a 'clear' function. See src/data/ClientProxy.js for details.");
66787 //</debug>
66788 }
66789 });
66790
66791 /**
66792 * @author Ed Spencer
66793 *
66794 * In-memory proxy. This proxy simply uses a local variable for data storage/retrieval, so its contents are lost on
66795 * every page refresh.
66796 *
66797 * Usually this Proxy isn't used directly, serving instead as a helper to a {@link Ext.data.Store Store} where a reader
66798 * is required to load data. For example, say we have a Store for a User model and have some inline data we want to
66799 * load, but this data isn't in quite the right format: we can use a MemoryProxy with a JsonReader to read it into our
66800 * Store:
66801 *
66802 * //this is the model we will be using in the store
66803 * Ext.define('User', {
66804 * extend: 'Ext.data.Model',
66805 * config: {
66806 * fields: [
66807 * {name: 'id', type: 'int'},
66808 * {name: 'name', type: 'string'},
66809 * {name: 'phone', type: 'string', mapping: 'phoneNumber'}
66810 * ]
66811 * }
66812 * });
66813 *
66814 * //this data does not line up to our model fields - the phone field is called phoneNumber
66815 * var data = {
66816 * users: [
66817 * {
66818 * id: 1,
66819 * name: 'Ed Spencer',
66820 * phoneNumber: '555 1234'
66821 * },
66822 * {
66823 * id: 2,
66824 * name: 'Abe Elias',
66825 * phoneNumber: '666 1234'
66826 * }
66827 * ]
66828 * };
66829 *
66830 * //note how we set the 'root' in the reader to match the data structure above
66831 * var store = Ext.create('Ext.data.Store', {
66832 * autoLoad: true,
66833 * model: 'User',
66834 * data : data,
66835 * proxy: {
66836 * type: 'memory',
66837 * reader: {
66838 * type: 'json',
66839 * rootProperty: 'users'
66840 * }
66841 * }
66842 * });
66843 *
66844 * ###Further Reading
66845 * [Sencha Touch Data Overview](../../../core_concepts/data/data_package_overview.html)
66846 * [Sencha Touch Store Guide](../../../core_concepts/data/stores.html)
66847 * [Sencha Touch Models Guide](../../../core_concepts/data/models.html)
66848 * [Sencha Touch Proxy Guide](../../../core_concepts/data/proxies.html)
66849 */
66850 Ext.define('Ext.data.proxy.Memory', {
66851 extend: Ext.data.proxy.Client ,
66852 alias: 'proxy.memory',
66853 alternateClassName: 'Ext.data.MemoryProxy',
66854
66855 isMemoryProxy: true,
66856
66857 config: {
66858 /**
66859 * @cfg {Object} data
66860 * Optional data to pass to configured Reader.
66861 */
66862 data: []
66863 },
66864
66865 /**
66866 * @private
66867 * Fake processing function to commit the records, set the current operation
66868 * to successful and call the callback if provided. This function is shared
66869 * by the create, update and destroy methods to perform the bare minimum
66870 * processing required for the proxy to register a result from the action.
66871 */
66872 finishOperation: function(operation, callback, scope) {
66873 if (operation) {
66874 var i = 0,
66875 recs = operation.getRecords(),
66876 len = recs.length;
66877
66878 for (i; i < len; i++) {
66879 recs[i].commit();
66880 }
66881
66882 operation.setCompleted();
66883 operation.setSuccessful();
66884
66885 Ext.callback(callback, scope || this, [operation]);
66886 }
66887 },
66888
66889 /**
66890 * Currently this is a hard-coded method that simply commits any records and sets the operation to successful,
66891 * then calls the callback function, if provided. It is essentially mocking a server call in memory, but since
66892 * there is no real back end in this case there's not much else to do. This method can be easily overridden to
66893 * implement more complex logic if needed.
66894 * @param {Ext.data.Operation} operation The Operation to perform
66895 * @param {Function} callback Callback function to be called when the Operation has completed (whether
66896 * successful or not)
66897 * @param {Object} scope Scope to execute the callback function in
66898 * @method
66899 */
66900 create: function() {
66901 this.finishOperation.apply(this, arguments);
66902 },
66903
66904 /**
66905 * Currently this is a hard-coded method that simply commits any records and sets the operation to successful,
66906 * then calls the callback function, if provided. It is essentially mocking a server call in memory, but since
66907 * there is no real back end in this case there's not much else to do. This method can be easily overridden to
66908 * implement more complex logic if needed.
66909 * @param {Ext.data.Operation} operation The Operation to perform
66910 * @param {Function} callback Callback function to be called when the Operation has completed (whether
66911 * successful or not)
66912 * @param {Object} scope Scope to execute the callback function in
66913 * @method
66914 */
66915 update: function() {
66916 this.finishOperation.apply(this, arguments);
66917 },
66918
66919 /**
66920 * Currently this is a hard-coded method that simply commits any records and sets the operation to successful,
66921 * then calls the callback function, if provided. It is essentially mocking a server call in memory, but since
66922 * there is no real back end in this case there's not much else to do. This method can be easily overridden to
66923 * implement more complex logic if needed.
66924 * @param {Ext.data.Operation} operation The Operation to perform
66925 * @param {Function} callback Callback function to be called when the Operation has completed (whether
66926 * successful or not)
66927 * @param {Object} scope Scope to execute the callback function in
66928 * @method
66929 */
66930 destroy: function() {
66931 this.finishOperation.apply(this, arguments);
66932 },
66933
66934 /**
66935 * Reads data from the configured {@link #data} object. Uses the Proxy's {@link #reader}, if present.
66936 * @param {Ext.data.Operation} operation The read Operation
66937 * @param {Function} callback The callback to call when reading has completed
66938 * @param {Object} scope The scope to call the callback function in
66939 */
66940 read: function(operation, callback, scope) {
66941 var me = this,
66942 reader = me.getReader();
66943
66944 if (operation.process('read', reader.process(me.getData())) === false) {
66945 this.fireEvent('exception', this, null, operation);
66946 }
66947
66948 Ext.callback(callback, scope || me, [operation]);
66949 },
66950
66951 clear: Ext.emptyFn
66952 });
66953
66954 /**
66955 * @author Ed Spencer
66956 * @class Ext.data.reader.Array
66957 *
66958 * Data reader class to create an Array of {@link Ext.data.Model} objects from an Array.
66959 * Each element of that Array represents a row of data fields. The
66960 * fields are pulled into a Record object using as a subscript, the `mapping` property
66961 * of the field definition if it exists, or the field's ordinal position in the definition.
66962 *
66963 * Example code:
66964 *
66965 * Employee = Ext.define('Employee', {
66966 * extend: 'Ext.data.Model',
66967 * config: {
66968 * fields: [
66969 * 'id',
66970 * {name: 'name', mapping: 1}, // "mapping" only needed if an "id" field is present which
66971 * {name: 'occupation', mapping: 2} // precludes using the ordinal position as the index.
66972 * ]
66973 * }
66974 * });
66975 *
66976 * var myReader = new Ext.data.reader.Array({
66977 * model: 'Employee'
66978 * }, Employee);
66979 *
66980 * This would consume an Array like this:
66981 *
66982 * [ [1, 'Bill', 'Gardener'], [2, 'Ben', 'Horticulturalist'] ]
66983 *
66984 * @constructor
66985 * Create a new ArrayReader
66986 * @param {Object} meta Metadata configuration options.
66987 */
66988 Ext.define('Ext.data.reader.Array', {
66989 extend: Ext.data.reader.Json ,
66990 alternateClassName: 'Ext.data.ArrayReader',
66991 alias : 'reader.array',
66992
66993 // For Array Reader, methods in the base which use these properties must not see the defaults
66994 config: {
66995 totalProperty: undefined,
66996 successProperty: undefined
66997 },
66998
66999 /**
67000 * @private
67001 * Returns an accessor expression for the passed Field from an Array using either the Field's mapping, or
67002 * its ordinal position in the fields collection as the index.
67003 * This is used by buildExtractors to create optimized on extractor function which converts raw data into model instances.
67004 */
67005 createFieldAccessExpression: function(field, fieldVarName, dataName) {
67006 var me = this,
67007 mapping = field.getMapping(),
67008 index = (mapping == null) ? me.getModel().getFields().indexOf(field) : mapping,
67009 result;
67010
67011 if (typeof index === 'function') {
67012 result = fieldVarName + '.getMapping()(' + dataName + ', this)';
67013 } else {
67014 if (isNaN(index)) {
67015 index = '"' + index + '"';
67016 }
67017 result = dataName + "[" + index + "]";
67018 }
67019 return result;
67020 }
67021 });
67022
67023 /**
67024 * @docauthor Evan Trimboli <evan@sencha.com>
67025 *
67026 * Contains a collection of all stores that are created that have an identifier. An identifier can be assigned by
67027 * setting the {@link Ext.data.Store#storeId storeId} property. When a store is in the StoreManager, it can be
67028 * referred to via it's identifier:
67029 *
67030 * Ext.create('Ext.data.Store', {
67031 * model: 'SomeModel',
67032 * storeId: 'myStore'
67033 * });
67034 *
67035 * var store = Ext.data.StoreManager.lookup('myStore');
67036 *
67037 * Also note that the {@link #lookup} method is aliased to {@link Ext#getStore} for convenience.
67038 *
67039 * If a store is registered with the StoreManager, you can also refer to the store by it's identifier when registering
67040 * it with any Component that consumes data from a store:
67041 *
67042 * Ext.create('Ext.data.Store', {
67043 * model: 'SomeModel',
67044 * storeId: 'myStore'
67045 * });
67046 *
67047 * Ext.create('Ext.view.View', {
67048 * store: 'myStore'
67049 * // other configuration here
67050 * });
67051 *
67052 * ###Further Reading
67053 * [Sencha Touch Data Overview](../../../core_concepts/data/data_package_overview.html)
67054 * [Sencha Touch Store Guide](../../../core_concepts/data/stores.html)
67055 * [Sencha Touch Models Guide](../../../core_concepts/data/models.html)
67056 * [Sencha Touch Proxy Guide](../../../core_concepts/data/proxies.html)
67057 */
67058 Ext.define('Ext.data.StoreManager', {
67059 extend: Ext.util.Collection ,
67060 alternateClassName: ['Ext.StoreMgr', 'Ext.data.StoreMgr', 'Ext.StoreManager'],
67061 singleton: true,
67062
67063
67064 /**
67065 * Registers one or more Stores with the StoreManager. You do not normally need to register stores manually. Any
67066 * store initialized with a {@link Ext.data.Store#storeId} will be auto-registered.
67067 * @param {Ext.data.Store...} stores Any number of Store instances.
67068 */
67069 register : function() {
67070 for (var i = 0, s; (s = arguments[i]); i++) {
67071 this.add(s);
67072 }
67073 },
67074
67075 /**
67076 * Unregisters one or more Stores with the StoreManager.
67077 * @param {String/Object...} stores Any number of Store instances or ID-s.
67078 */
67079 unregister : function() {
67080 for (var i = 0, s; (s = arguments[i]); i++) {
67081 this.remove(this.lookup(s));
67082 }
67083 },
67084
67085 /**
67086 * Gets a registered Store by its id, returns a passed store instance, or returns a new instance of a store created with the supplied configuration.
67087 * @param {String/Object} store The `id` of the Store, or a Store instance, or a store configuration.
67088 * @return {Ext.data.Store}
67089 */
67090 lookup : function(store) {
67091 // handle the case when we are given an array or an array of arrays.
67092 if (Ext.isArray(store)) {
67093 var fields = ['field1'],
67094 expand = !Ext.isArray(store[0]),
67095 data = store,
67096 i,
67097 len;
67098
67099 if (expand) {
67100 data = [];
67101 for (i = 0, len = store.length; i < len; ++i) {
67102 data.push([store[i]]);
67103 }
67104 } else {
67105 for(i = 2, len = store[0].length; i <= len; ++i){
67106 fields.push('field' + i);
67107 }
67108 }
67109 return Ext.create('Ext.data.ArrayStore', {
67110 data : data,
67111 fields: fields,
67112 // See https://sencha.jira.com/browse/TOUCH-1541
67113 autoDestroy: true,
67114 autoCreated: true,
67115 expanded: expand
67116 });
67117 }
67118
67119 if (Ext.isString(store)) {
67120 // store id
67121 return this.get(store);
67122 } else {
67123 // store instance or store config
67124 if (store instanceof Ext.data.Store) {
67125 return store;
67126 } else {
67127 return Ext.factory(store, Ext.data.Store, null, 'store');
67128 }
67129 }
67130 },
67131
67132 // getKey implementation for MixedCollection
67133 getKey : function(o) {
67134 return o.getStoreId();
67135 }
67136 }, function() {
67137 /**
67138 * Creates a new store for the given id and config, then registers it with the {@link Ext.data.StoreManager Store Manager}.
67139 * Sample usage:
67140 *
67141 * Ext.regStore('AllUsers', {
67142 * model: 'User'
67143 * });
67144 *
67145 * // the store can now easily be used throughout the application
67146 * new Ext.List({
67147 * store: 'AllUsers'
67148 * // ...
67149 * });
67150 *
67151 * @param {String} id The id to set on the new store.
67152 * @param {Object} config The store config.
67153 * @member Ext
67154 * @method regStore
67155 */
67156 Ext.regStore = function(name, config) {
67157 var store;
67158
67159 if (Ext.isObject(name)) {
67160 config = name;
67161 } else {
67162 if (config instanceof Ext.data.Store) {
67163 config.setStoreId(name);
67164 } else {
67165 config.storeId = name;
67166 }
67167 }
67168
67169 if (config instanceof Ext.data.Store) {
67170 store = config;
67171 } else {
67172 store = Ext.create('Ext.data.Store', config);
67173 }
67174
67175 return Ext.data.StoreManager.register(store);
67176 };
67177
67178 /**
67179 * Shortcut to {@link Ext.data.StoreManager#lookup}.
67180 * @member Ext
67181 * @method getStore
67182 * @alias Ext.data.StoreManager#lookup
67183 */
67184 Ext.getStore = function(name) {
67185 return Ext.data.StoreManager.lookup(name);
67186 };
67187 });
67188
67189 /**
67190 * Tracks what records are currently selected in a databound widget. This class is mixed in to {@link Ext.dataview.DataView} and
67191 * all subclasses.
67192 * @private
67193 */
67194 Ext.define('Ext.mixin.Selectable', {
67195 extend: Ext.mixin.Mixin ,
67196
67197 mixinConfig: {
67198 id: 'selectable',
67199 hooks: {
67200 updateStore: 'updateStore'
67201 }
67202 },
67203
67204 /**
67205 * @event beforeselectionchange
67206 * Fires before an item is selected.
67207 * @param {Ext.mixin.Selectable} this
67208 * @preventable selectionchange
67209 * @deprecated 2.0.0 Please listen to the {@link #selectionchange} event with an order of `before` instead.
67210 */
67211
67212 /**
67213 * @event selectionchange
67214 * Fires when a selection changes.
67215 * @param {Ext.mixin.Selectable} this
67216 * @param {Ext.data.Model[]} records The records whose selection has changed.
67217 */
67218
67219 config: {
67220 /**
67221 * @cfg {Boolean} disableSelection `true` to disable selection.
67222 * This configuration will lock the selection model that the DataView uses.
67223 * @accessor
67224 */
67225 disableSelection: null,
67226
67227 /**
67228 * @cfg {String} mode
67229 * Modes of selection.
67230 * Valid values are `'SINGLE'`, `'SIMPLE'`, and `'MULTI'`.
67231 * @accessor
67232 */
67233 mode: 'SINGLE',
67234
67235 /**
67236 * @cfg {Boolean} allowDeselect
67237 * Allow users to deselect a record in a DataView, List or Grid. Only applicable when the Selectable's `mode` is
67238 * `'SINGLE'`.
67239 * @accessor
67240 */
67241 allowDeselect: false,
67242
67243 /**
67244 * @cfg {Ext.data.Model} lastSelected
67245 * @private
67246 * @accessor
67247 */
67248 lastSelected: null,
67249
67250 /**
67251 * @cfg {Ext.data.Model} lastFocused
67252 * @private
67253 * @accessor
67254 */
67255 lastFocused: null,
67256
67257 /**
67258 * @cfg {Boolean} deselectOnContainerClick `true` to deselect current selection when the container body is
67259 * clicked.
67260 * @accessor
67261 */
67262 deselectOnContainerClick: true
67263 },
67264
67265 modes: {
67266 SINGLE: true,
67267 SIMPLE: true,
67268 MULTI: true
67269 },
67270
67271 selectableEventHooks: {
67272 addrecords: 'onSelectionStoreAdd',
67273 removerecords: 'onSelectionStoreRemove',
67274 updaterecord: 'onSelectionStoreUpdate',
67275 load: 'refreshSelection',
67276 refresh: 'refreshSelection'
67277 },
67278
67279 constructor: function() {
67280 this.selected = new Ext.util.MixedCollection();
67281 this.callParent(arguments);
67282 },
67283
67284 /**
67285 * @private
67286 */
67287 applyMode: function(mode) {
67288 mode = mode ? mode.toUpperCase() : 'SINGLE';
67289 // set to mode specified unless it doesnt exist, in that case
67290 // use single.
67291 return this.modes[mode] ? mode : 'SINGLE';
67292 },
67293
67294 /**
67295 * @private
67296 */
67297 updateStore: function(newStore, oldStore) {
67298 var me = this,
67299 bindEvents = Ext.apply({}, me.selectableEventHooks, { scope: me });
67300
67301 if (oldStore && Ext.isObject(oldStore) && oldStore.isStore) {
67302 if (oldStore.autoDestroy) {
67303 oldStore.destroy();
67304 }
67305 else {
67306 oldStore.un(bindEvents);
67307 if(newStore) {
67308 newStore.un('clear', 'onSelectionStoreClear', this);
67309 }
67310 }
67311 }
67312
67313 if (newStore) {
67314 newStore.on(bindEvents);
67315 newStore.onBefore('clear', 'onSelectionStoreClear', this);
67316 me.refreshSelection();
67317 }
67318 },
67319
67320 /**
67321 * Selects all records.
67322 * @param {Boolean} silent `true` to suppress all select events.
67323 */
67324 selectAll: function(silent) {
67325 var me = this,
67326 selections = me.getStore().getRange();
67327
67328 me.select(selections, true, silent);
67329 },
67330
67331 /**
67332 * Deselects all records.
67333 */
67334 deselectAll: function(supress) {
67335 var me = this,
67336 selections = me.getStore().getRange();
67337
67338 me.deselect(selections, supress);
67339
67340 me.selected.clear();
67341 me.setLastSelected(null);
67342 me.setLastFocused(null);
67343 },
67344
67345 // Provides differentiation of logic between MULTI, SIMPLE and SINGLE
67346 // selection modes.
67347 selectWithEvent: function(record) {
67348 var me = this,
67349 isSelected = me.isSelected(record);
67350 switch (me.getMode()) {
67351 case 'MULTI':
67352 case 'SIMPLE':
67353 if (isSelected) {
67354 me.deselect(record);
67355 }
67356 else {
67357 me.select(record, true);
67358 }
67359 break;
67360 case 'SINGLE':
67361 if (me.getAllowDeselect() && isSelected) {
67362 // if allowDeselect is on and this record isSelected, deselect it
67363 me.deselect(record);
67364 } else {
67365 // select the record and do NOT maintain existing selections
67366 me.select(record, false);
67367 }
67368 break;
67369 }
67370 },
67371
67372 /**
67373 * Selects a range of rows if the selection model {@link Ext.mixin.Selectable#getDisableSelection} is not locked.
67374 * All rows in between `startRecord` and `endRecord` are also selected.
67375 * @param {Number} startRecord The index of the first row in the range.
67376 * @param {Number} endRecord The index of the last row in the range.
67377 * @param {Boolean} [keepExisting] `true` to retain existing selections.
67378 */
67379 selectRange: function(startRecord, endRecord, keepExisting) {
67380 var me = this,
67381 store = me.getStore(),
67382 records = [],
67383 tmp, i;
67384
67385 if (me.getDisableSelection()) {
67386 return;
67387 }
67388
67389 // swap values
67390 if (startRecord > endRecord) {
67391 tmp = endRecord;
67392 endRecord = startRecord;
67393 startRecord = tmp;
67394 }
67395
67396 for (i = startRecord; i <= endRecord; i++) {
67397 records.push(store.getAt(i));
67398 }
67399 this.doMultiSelect(records, keepExisting);
67400 },
67401
67402 /**
67403 * Adds the given records to the currently selected set.
67404 * @param {Ext.data.Model/Array/Number} records The records to select.
67405 * @param {Boolean} keepExisting If `true`, the existing selection will be added to (if not, the old selection is replaced).
67406 * @param {Boolean} suppressEvent If `true`, the `select` event will not be fired.
67407 */
67408 select: function(records, keepExisting, suppressEvent) {
67409 var me = this,
67410 record;
67411
67412 if (me.getDisableSelection()) {
67413 return;
67414 }
67415
67416 if (typeof records === "number") {
67417 records = [me.getStore().getAt(records)];
67418 }
67419
67420 if (!records) {
67421 return;
67422 }
67423
67424 if (me.getMode() == "SINGLE" && records) {
67425 record = records.length ? records[0] : records;
67426 me.doSingleSelect(record, suppressEvent);
67427 } else {
67428 me.doMultiSelect(records, keepExisting, suppressEvent);
67429 }
67430 },
67431
67432 /**
67433 * Selects a single record.
67434 * @private
67435 */
67436 doSingleSelect: function(record, suppressEvent) {
67437 var me = this,
67438 selected = me.selected;
67439
67440 if (me.getDisableSelection()) {
67441 return;
67442 }
67443
67444 // already selected.
67445 // should we also check beforeselect?
67446 if (me.isSelected(record)) {
67447 return;
67448 }
67449
67450 if (selected.getCount() > 0) {
67451 me.deselect(me.getLastSelected(), suppressEvent);
67452 }
67453
67454 selected.add(record);
67455 me.setLastSelected(record);
67456 me.onItemSelect(record, suppressEvent);
67457 me.setLastFocused(record);
67458
67459 if (!suppressEvent) {
67460 me.fireSelectionChange([record]);
67461 }
67462 },
67463
67464 /**
67465 * Selects a set of multiple records.
67466 * @private
67467 */
67468 doMultiSelect: function(records, keepExisting, suppressEvent) {
67469 if (records === null || this.getDisableSelection()) {
67470 return;
67471 }
67472 records = !Ext.isArray(records) ? [records] : records;
67473
67474 var me = this,
67475 selected = me.selected,
67476 ln = records.length,
67477 change = false,
67478 i = 0,
67479 record;
67480
67481 if (!keepExisting && selected.getCount() > 0) {
67482 change = true;
67483 me.deselect(me.getSelection(), true);
67484 }
67485 for (; i < ln; i++) {
67486 record = records[i];
67487 if (keepExisting && me.isSelected(record)) {
67488 continue;
67489 }
67490 change = true;
67491 me.setLastSelected(record);
67492 selected.add(record);
67493 if (!suppressEvent) {
67494 me.setLastFocused(record);
67495 }
67496
67497 me.onItemSelect(record, suppressEvent);
67498 }
67499 if (change && !suppressEvent) {
67500 this.fireSelectionChange(records);
67501 }
67502 },
67503
67504 /**
67505 * Deselects the given record(s). If many records are currently selected, it will only deselect those you pass in.
67506 * @param {Number/Array/Ext.data.Model} records The record(s) to deselect. Can also be a number to reference by index.
67507 * @param {Boolean} suppressEvent If `true` the `deselect` event will not be fired.
67508 */
67509 deselect: function(records, suppressEvent) {
67510 var me = this;
67511
67512 if (me.getDisableSelection()) {
67513 return;
67514 }
67515
67516 records = Ext.isArray(records) ? records : [records];
67517
67518 var selected = me.selected,
67519 change = false,
67520 i = 0,
67521 store = me.getStore(),
67522 ln = records.length,
67523 record;
67524
67525 for (; i < ln; i++) {
67526 record = records[i];
67527
67528 if (typeof record === 'number') {
67529 record = store.getAt(record);
67530 }
67531
67532 if (selected.remove(record)) {
67533 if (me.getLastSelected() == record) {
67534 me.setLastSelected(selected.last());
67535 }
67536 change = true;
67537 }
67538 if (record) {
67539 me.onItemDeselect(record, suppressEvent);
67540 }
67541 }
67542
67543 if (change && !suppressEvent) {
67544 me.fireSelectionChange(records);
67545 }
67546 },
67547
67548 /**
67549 * Sets a record as the last focused record. This does NOT mean
67550 * that the record has been selected.
67551 * @param {Ext.data.Record} newRecord
67552 * @param {Ext.data.Record} oldRecord
67553 */
67554 updateLastFocused: function(newRecord, oldRecord) {
67555 this.onLastFocusChanged(oldRecord, newRecord);
67556 },
67557
67558 fireSelectionChange: function(records) {
67559 var me = this;
67560 me.fireAction('selectionchange', [me, records], 'getSelection');
67561 },
67562
67563 /**
67564 * Returns an array of the currently selected records.
67565 * @return {Array} An array of selected records.
67566 */
67567 getSelection: function() {
67568 return this.selected.getRange();
67569 },
67570
67571 /**
67572 * Returns `true` if the specified row is selected.
67573 * @param {Ext.data.Model/Number} record The record or index of the record to check.
67574 * @return {Boolean}
67575 */
67576 isSelected: function(record) {
67577 record = Ext.isNumber(record) ? this.getStore().getAt(record) : record;
67578 return this.selected.indexOf(record) !== -1;
67579 },
67580
67581 /**
67582 * Returns `true` if there is a selected record.
67583 * @return {Boolean}
67584 */
67585 hasSelection: function() {
67586 return this.selected.getCount() > 0;
67587 },
67588
67589 /**
67590 * @private
67591 */
67592 refreshSelection: function() {
67593 var me = this,
67594 selections = me.getSelection();
67595
67596 me.deselectAll(true);
67597 if (selections.length) {
67598 me.select(selections, false, true);
67599 }
67600 },
67601
67602 // prune records from the SelectionModel if
67603 // they were selected at the time they were
67604 // removed.
67605 onSelectionStoreRemove: function(store, records) {
67606 var me = this,
67607 selected = me.selected,
67608 ln = records.length,
67609 record, i;
67610
67611 if (me.getDisableSelection()) {
67612 return;
67613 }
67614
67615 for (i = 0; i < ln; i++) {
67616 record = records[i];
67617 if (selected.remove(record)) {
67618 if (me.getLastSelected() == record) {
67619 me.setLastSelected(null);
67620 }
67621 if (me.getLastFocused() == record) {
67622 me.setLastFocused(null);
67623 }
67624 me.fireSelectionChange([record]);
67625 }
67626 }
67627 },
67628
67629 onSelectionStoreClear: function(store) {
67630 var records = store.getData().items;
67631 this.onSelectionStoreRemove(store, records);
67632 },
67633
67634 /**
67635 * Returns the number of selections.
67636 * @return {Number}
67637 */
67638 getSelectionCount: function() {
67639 return this.selected.getCount();
67640 },
67641
67642 onSelectionStoreAdd: Ext.emptyFn,
67643 onSelectionStoreUpdate: Ext.emptyFn,
67644 onItemSelect: Ext.emptyFn,
67645 onItemDeselect: Ext.emptyFn,
67646 onLastFocusChanged: Ext.emptyFn,
67647 onEditorKey: Ext.emptyFn
67648 }, function() {
67649 /**
67650 * Selects a record instance by record instance or index.
67651 * @member Ext.mixin.Selectable
67652 * @method doSelect
67653 * @param {Ext.data.Model/Number} records An array of records or an index.
67654 * @param {Boolean} keepExisting
67655 * @param {Boolean} suppressEvent Set to `false` to not fire a select event.
67656 * @deprecated 2.0.0 Please use {@link #select} instead.
67657 */
67658
67659 /**
67660 * Deselects a record instance by record instance or index.
67661 * @member Ext.mixin.Selectable
67662 * @method doDeselect
67663 * @param {Ext.data.Model/Number} records An array of records or an index.
67664 * @param {Boolean} suppressEvent Set to `false` to not fire a deselect event.
67665 * @deprecated 2.0.0 Please use {@link #deselect} instead.
67666 */
67667
67668 /**
67669 * Returns the selection mode currently used by this Selectable.
67670 * @member Ext.mixin.Selectable
67671 * @method getSelectionMode
67672 * @return {String} The current mode.
67673 * @deprecated 2.0.0 Please use {@link #getMode} instead.
67674 */
67675
67676 /**
67677 * Returns the array of previously selected items.
67678 * @member Ext.mixin.Selectable
67679 * @method getLastSelected
67680 * @return {Array} The previous selection.
67681 * @deprecated 2.0.0
67682 */
67683
67684 /**
67685 * Returns `true` if the Selectable is currently locked.
67686 * @member Ext.mixin.Selectable
67687 * @method isLocked
67688 * @return {Boolean} True if currently locked
67689 * @deprecated 2.0.0 Please use {@link #getDisableSelection} instead.
67690 */
67691
67692 /**
67693 * This was an internal function accidentally exposed in 1.x and now deprecated. Calling it has no effect
67694 * @member Ext.mixin.Selectable
67695 * @method setLastFocused
67696 * @deprecated 2.0.0
67697 */
67698
67699 /**
67700 * Deselects any currently selected records and clears all stored selections.
67701 * @member Ext.mixin.Selectable
67702 * @method clearSelections
67703 * @deprecated 2.0.0 Please use {@link #deselectAll} instead.
67704 */
67705
67706 /**
67707 * Returns the number of selections.
67708 * @member Ext.mixin.Selectable
67709 * @method getCount
67710 * @return {Number}
67711 * @deprecated 2.0.0 Please use {@link #getSelectionCount} instead.
67712 */
67713
67714 /**
67715 * @cfg {Boolean} locked
67716 * @inheritdoc Ext.mixin.Selectable#disableSelection
67717 * @deprecated 2.0.0 Please use {@link #disableSelection} instead.
67718 */
67719 });
67720
67721 /**
67722 * A DataItem is a container for records inside of {@link Ext.dataview.DataView} with useComponents: true.
67723 * It ties together {@link Ext.data.Model records} to its contained Components. Consider the following example:
67724 *
67725 * @example phone portrait preview
67726 * // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! MODEL
67727 *
67728 * Ext.define('TestModel', {
67729 * extend: 'Ext.data.Model',
67730 * config: {
67731 * fields: [{
67732 * name: 'val1'
67733 * }, {
67734 * name: 'val2'
67735 * }]
67736 * }
67737 * });
67738 *
67739 * // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! STORE
67740 *
67741 * Ext.define('TestStore', {
67742 * extend: 'Ext.data.Store',
67743 * config: {
67744 * data: [{
67745 * val1: 'A Button',
67746 * val2: 'with text'
67747 * }, {
67748 * val1: 'The Button',
67749 * val2: 'more text'
67750 * }, {
67751 * val1: 'My Button',
67752 * val2: 'My Text'
67753 * }],
67754 * model: 'TestModel',
67755 * storeId: 'TestStore'
67756 * }
67757 * });
67758 *
67759 * // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! DATA ITEM
67760 *
67761 * Ext.define('MyDataItem', {
67762 * extend: 'Ext.dataview.component.DataItem',
67763 * alias: 'widget.mydataitem',
67764 * config: {
67765 * padding: 10,
67766 * layout: {
67767 * type: 'hbox'
67768 * },
67769 * defaults: {
67770 * margin: 5
67771 * },
67772 * items: [{
67773 * xtype: 'button',
67774 * text: 'Val1'
67775 * }, {
67776 * xtype: 'component',
67777 * flex: 1,
67778 * html: 'val2',
67779 * itemId: 'textCmp'
67780 * }]
67781 * },
67782 * updateRecord: function(record) {
67783 * var me = this;
67784 *
67785 * me.down('button').setText(record.get('val1'));
67786 * me.down('#textCmp').setHtml(record.get('val2'));
67787 *
67788 * me.callParent(arguments);
67789 * }
67790 * });
67791 *
67792 * // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! DATA VIEW
67793 *
67794 * Ext.define('MyDataView', {
67795 * extend: 'Ext.dataview.DataView',
67796 * config: {
67797 * defaultType: 'mydataitem',
67798 * useComponents: true
67799 * }
67800 * });
67801 *
67802 * // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! RUN
67803 *
67804 * Ext.create('MyDataView', {
67805 * fullscreen: true,
67806 * store: Ext.create('TestStore')
67807 * });
67808 *
67809 * Another way to accomplish this is via a {@link #dataMap dataMap} configuration.
67810 *
67811 * For example, lets say you have a `text` configuration which, when applied, gets turned into an instance of an
67812 * Ext.Component. We want to update the {@link #html} of a sub-component when the 'text' field of the record gets
67813 * changed.
67814 *
67815 * As you can see below, it is simply a matter of setting the key of the object to be the getter of the config
67816 * (getText), and then give that property a value of an object, which then has 'setHtml' (the html setter) as the key,
67817 * and 'text' (the field name) as the value. You can continue this for a as many sub-components as you wish.
67818 *
67819 * dataMap: {
67820 * // When the record is updated, get the text configuration, and
67821 * // call {@link #setHtml} with the 'text' field of the record.
67822 * getText: {
67823 * setHtml: 'text'
67824 * },
67825 *
67826 * // When the record is updated, get the userName configuration, and
67827 * // call {@link #setHtml} with the 'from_user' field of the record.
67828 * getUserName: {
67829 * setHtml: 'from_user'
67830 * },
67831 *
67832 * // When the record is updated, get the avatar configuration, and
67833 * // call `setSrc` with the 'profile_image_url' field of the record.
67834 * getAvatar: {
67835 * setSrc: 'profile_image_url'
67836 * }
67837 * }
67838 */
67839
67840 Ext.define('Ext.dataview.component.DataItem', {
67841 extend: Ext.Container ,
67842 xtype : 'dataitem',
67843
67844 config: {
67845 baseCls: Ext.baseCSSPrefix + 'data-item',
67846
67847 defaultType: 'component',
67848
67849 /**
67850 * @cfg {Ext.data.Model} record The model instance of this DataItem. It is controlled by the Component DataView.
67851 * @accessor
67852 */
67853 record: null,
67854
67855 /**
67856 * @cfg {String} itemCls
67857 * An additional CSS class to apply to items within the DataView.
67858 * @accessor
67859 */
67860 itemCls: null,
67861
67862 /**
67863 * @cfg dataMap
67864 * The dataMap allows you to map {@link #record} fields to specific configurations in this component.
67865 *
67866 * For example, lets say you have a `text` configuration which, when applied, gets turned into an instance of an Ext.Component.
67867 * We want to update the {@link #html} of this component when the 'text' field of the record gets changed.
67868 * For example:
67869 *
67870 * dataMap: {
67871 * getText: {
67872 * setHtml: 'text'
67873 * }
67874 * }
67875 *
67876 * In this example, it is simply a matter of setting the key of the object to be the getter of the config (`getText`), and then give that
67877 * property a value of an object, which then has `setHtml` (the html setter) as the key, and `text` (the field name) as the value.
67878 */
67879 dataMap: {},
67880
67881 /*
67882 * @private dataview
67883 */
67884 dataview: null,
67885
67886 width: '100%',
67887
67888 items: [{
67889 xtype: 'component'
67890 }]
67891 },
67892
67893 updateBaseCls: function(newBaseCls, oldBaseCls) {
67894 var me = this;
67895
67896 me.callParent(arguments);
67897 },
67898
67899 updateItemCls: function(newCls, oldCls) {
67900 if (oldCls) {
67901 this.removeCls(oldCls);
67902 }
67903 if (newCls) {
67904 this.addCls(newCls);
67905 }
67906 },
67907
67908 doMapData: function(dataMap, data, item) {
67909 var componentName, component, setterMap, setterName;
67910
67911 for (componentName in dataMap) {
67912 setterMap = dataMap[componentName];
67913 component = this[componentName]();
67914 if (component) {
67915 for (setterName in setterMap) {
67916 if (data && component[setterName] && data[setterMap[setterName]] !== undefined && data[setterMap[setterName]] !== null) {
67917 component[setterName](data[setterMap[setterName]]);
67918 }
67919 }
67920 }
67921 }
67922
67923 if (item) {
67924 // Bypassing setter because sometimes we pass the same object (different properties)
67925 item.updateData(data);
67926 }
67927 },
67928
67929 /**
67930 * Updates this container's child items, passing through the `dataMap`.
67931 * @param {Ext.data.Model} newRecord
67932 * @private
67933 */
67934 updateRecord: function(newRecord) {
67935 if (!newRecord) {
67936 return;
67937 }
67938 this._record = newRecord;
67939
67940 var me = this,
67941 dataview = me.dataview || this.getDataview(),
67942 data = dataview.prepareData(newRecord.getData(true), dataview.getStore().indexOf(newRecord), newRecord),
67943 items = me.getItems(),
67944 item = items.first(),
67945 dataMap = me.getDataMap();
67946
67947 if (!item) {
67948 return;
67949 }
67950 if (dataMap) {
67951 this.doMapData(dataMap, data, item);
67952 }
67953
67954 /**
67955 * @event updatedata
67956 * Fires whenever the data of the DataItem is updated.
67957 * @param {Ext.dataview.component.DataItem} this The DataItem instance.
67958 * @param {Object} newData The new data.
67959 */
67960 me.fireEvent('updatedata', me, data);
67961 }
67962 });
67963
67964 /**
67965 * @private
67966 */
67967 Ext.define('Ext.dataview.component.Container', {
67968 extend: Ext.Container ,
67969
67970
67971
67972
67973
67974 /**
67975 * @event itemtouchstart
67976 * Fires whenever an item is touched
67977 * @param {Ext.dataview.component.Container} this
67978 * @param {Ext.dataview.component.DataItem} item The item touched
67979 * @param {Number} index The index of the item touched
67980 * @param {Ext.EventObject} e The event object
67981 */
67982
67983 /**
67984 * @event itemtouchmove
67985 * Fires whenever an item is moved
67986 * @param {Ext.dataview.component.Container} this
67987 * @param {Ext.dataview.component.DataItem} item The item moved
67988 * @param {Number} index The index of the item moved
67989 * @param {Ext.EventObject} e The event object
67990 */
67991
67992 /**
67993 * @event itemtouchend
67994 * Fires whenever an item is touched
67995 * @param {Ext.dataview.component.Container} this
67996 * @param {Ext.dataview.component.DataItem} item The item touched
67997 * @param {Number} index The index of the item touched
67998 * @param {Ext.EventObject} e The event object
67999 */
68000
68001 /**
68002 * @event itemtap
68003 * Fires whenever an item is tapped
68004 * @param {Ext.dataview.component.Container} this
68005 * @param {Ext.dataview.component.DataItem} item The item tapped
68006 * @param {Number} index The index of the item tapped
68007 * @param {Ext.EventObject} e The event object
68008 */
68009
68010 /**
68011 * @event itemtaphold
68012 * Fires whenever an item is tapped
68013 * @param {Ext.dataview.component.Container} this
68014 * @param {Ext.dataview.component.DataItem} item The item tapped
68015 * @param {Number} index The index of the item tapped
68016 * @param {Ext.EventObject} e The event object
68017 */
68018
68019 /**
68020 * @event itemsingletap
68021 * Fires whenever an item is doubletapped
68022 * @param {Ext.dataview.component.Container} this
68023 * @param {Ext.dataview.component.DataItem} item The item singletapped
68024 * @param {Number} index The index of the item singletapped
68025 * @param {Ext.EventObject} e The event object
68026 */
68027
68028 /**
68029 * @event itemdoubletap
68030 * Fires whenever an item is doubletapped
68031 * @param {Ext.dataview.component.Container} this
68032 * @param {Ext.dataview.component.DataItem} item The item doubletapped
68033 * @param {Number} index The index of the item doubletapped
68034 * @param {Ext.EventObject} e The event object
68035 */
68036
68037 /**
68038 * @event itemswipe
68039 * Fires whenever an item is swiped
68040 * @param {Ext.dataview.component.Container} this
68041 * @param {Ext.dataview.component.DataItem} item The item swiped
68042 * @param {Number} index The index of the item swiped
68043 * @param {Ext.EventObject} e The event object
68044 */
68045
68046 constructor: function() {
68047 this.itemCache = [];
68048 this.callParent(arguments);
68049 },
68050
68051 //@private
68052 doInitialize: function() {
68053 this.innerElement.on({
68054 touchstart: 'onItemTouchStart',
68055 touchend: 'onItemTouchEnd',
68056 tap: 'onItemTap',
68057 taphold: 'onItemTapHold',
68058 touchmove: 'onItemTouchMove',
68059 singletap: 'onItemSingleTap',
68060 doubletap: 'onItemDoubleTap',
68061 swipe: 'onItemSwipe',
68062 delegate: '> .' + Ext.baseCSSPrefix + 'data-item',
68063 scope: this
68064 });
68065 },
68066
68067 //@private
68068 initialize: function() {
68069 this.callParent();
68070 this.doInitialize();
68071 },
68072
68073 onItemTouchStart: function(e) {
68074 var me = this,
68075 target = e.getTarget(),
68076 item = Ext.getCmp(target.id);
68077
68078 item.on({
68079 touchmove: 'onItemTouchMove',
68080 scope : me,
68081 single: true
68082 });
68083
68084 me.fireEvent('itemtouchstart', me, item, me.indexOf(item), e);
68085 },
68086
68087 onItemTouchMove: function(e) {
68088 var me = this,
68089 target = e.getTarget(),
68090 item = Ext.getCmp(target.id);
68091 me.fireEvent('itemtouchmove', me, item, me.indexOf(item), e);
68092 },
68093
68094 onItemTouchEnd: function(e) {
68095 var me = this,
68096 target = e.getTarget(),
68097 item = Ext.getCmp(target.id);
68098
68099 item.un({
68100 touchmove: 'onItemTouchMove',
68101 scope : me
68102 });
68103
68104 me.fireEvent('itemtouchend', me, item, me.indexOf(item), e);
68105 },
68106
68107 onItemTap: function(e) {
68108 var me = this,
68109 target = e.getTarget(),
68110 item = Ext.getCmp(target.id);
68111 me.fireEvent('itemtap', me, item, me.indexOf(item), e);
68112 },
68113
68114 onItemTapHold: function(e) {
68115 var me = this,
68116 target = e.getTarget(),
68117 item = Ext.getCmp(target.id);
68118 me.fireEvent('itemtaphold', me, item, me.indexOf(item), e);
68119 },
68120
68121 onItemSingleTap: function(e) {
68122 var me = this,
68123 target = e.getTarget(),
68124 item = Ext.getCmp(target.id);
68125 me.fireEvent('itemsingletap', me, item, me.indexOf(item), e);
68126 },
68127
68128 onItemDoubleTap: function(e) {
68129 var me = this,
68130 target = e.getTarget(),
68131 item = Ext.getCmp(target.id);
68132 me.fireEvent('itemdoubletap', me, item, me.indexOf(item), e);
68133 },
68134
68135 onItemSwipe: function(e) {
68136 var me = this,
68137 target = e.getTarget(),
68138 item = Ext.getCmp(target.id);
68139 me.fireEvent('itemswipe', me, item, me.indexOf(item), e);
68140 },
68141
68142 moveItemsToCache: function(from, to) {
68143 var me = this,
68144 dataview = me.dataview,
68145 maxItemCache = dataview.getMaxItemCache(),
68146 items = me.getViewItems(),
68147 itemCache = me.itemCache,
68148 cacheLn = itemCache.length,
68149 pressedCls = dataview.getPressedCls(),
68150 selectedCls = dataview.getSelectedCls(),
68151 i = to - from,
68152 item;
68153
68154 for (; i >= 0; i--) {
68155 item = items[from + i];
68156 if (cacheLn !== maxItemCache) {
68157 me.remove(item, false);
68158 item.removeCls([pressedCls, selectedCls]);
68159 itemCache.push(item);
68160 cacheLn++;
68161 }
68162 else {
68163 item.destroy();
68164 }
68165 }
68166
68167 if (me.getViewItems().length == 0) {
68168 this.dataview.showEmptyText();
68169 }
68170 },
68171
68172 moveItemsFromCache: function(records) {
68173 var me = this,
68174 dataview = me.dataview,
68175 store = dataview.getStore(),
68176 ln = records.length,
68177 xtype = dataview.getDefaultType(),
68178 itemConfig = dataview.getItemConfig(),
68179 itemCache = me.itemCache,
68180 cacheLn = itemCache.length,
68181 items = [],
68182 i, item, record;
68183
68184 if (ln) {
68185 dataview.hideEmptyText();
68186 }
68187
68188 for (i = 0; i < ln; i++) {
68189 records[i]._tmpIndex = store.indexOf(records[i]);
68190 }
68191
68192 Ext.Array.sort(records, function(record1, record2) {
68193 return record1._tmpIndex > record2._tmpIndex ? 1 : -1;
68194 });
68195
68196 for (i = 0; i < ln; i++) {
68197 record = records[i];
68198 if (cacheLn) {
68199 cacheLn--;
68200 item = itemCache.pop();
68201 this.updateListItem(record, item);
68202 }
68203 else {
68204 item = me.getDataItemConfig(xtype, record, itemConfig);
68205 }
68206 item = this.insert(record._tmpIndex, item);
68207 delete record._tmpIndex;
68208 }
68209 return items;
68210 },
68211
68212 getViewItems: function() {
68213 return this.getInnerItems();
68214 },
68215
68216 updateListItem: function(record, item) {
68217 if (item.updateRecord) {
68218 if (item.getRecord() === record) {
68219 item.updateRecord(record);
68220 } else {
68221 item.setRecord(record);
68222 }
68223 }
68224 },
68225
68226 getDataItemConfig: function(xtype, record, itemConfig) {
68227 var dataview = this.dataview,
68228 dataItemConfig = {
68229 xtype: xtype,
68230 record: record,
68231 itemCls: dataview.getItemCls(),
68232 defaults: itemConfig,
68233 dataview: dataview
68234 };
68235 return Ext.merge(dataItemConfig, itemConfig);
68236 },
68237
68238 doRemoveItemCls: function(cls) {
68239 var items = this.getViewItems(),
68240 ln = items.length,
68241 i = 0;
68242
68243 for (; i < ln; i++) {
68244 items[i].removeCls(cls);
68245 }
68246 },
68247
68248 doAddItemCls: function(cls) {
68249 var items = this.getViewItems(),
68250 ln = items.length,
68251 i = 0;
68252
68253 for (; i < ln; i++) {
68254 items[i].addCls(cls);
68255 }
68256 },
68257
68258 updateAtNewIndex: function(oldIndex, newIndex, record) {
68259 this.moveItemsToCache(oldIndex, oldIndex);
68260 this.moveItemsFromCache([record]);
68261 },
68262
68263 destroy: function() {
68264 var me = this,
68265 itemCache = me.itemCache,
68266 ln = itemCache.length,
68267 i = 0;
68268
68269 for (; i < ln; i++) {
68270 itemCache[i].destroy();
68271 }
68272 this.callParent();
68273 }
68274 });
68275
68276 /**
68277 * @private
68278 */
68279 Ext.define('Ext.dataview.element.Container', {
68280 extend: Ext.Component ,
68281
68282 /**
68283 * @event itemtouchstart
68284 * Fires whenever an item is touched
68285 * @param {Ext.dataview.element.Container} this
68286 * @param {Ext.dom.Element} item The item touched
68287 * @param {Number} index The index of the item touched
68288 * @param {Ext.EventObject} e The event object
68289 */
68290
68291 /**
68292 * @event itemtouchmove
68293 * Fires whenever an item is moved
68294 * @param {Ext.dataview.element.Container} this
68295 * @param {Ext.dom.Element} item The item moved
68296 * @param {Number} index The index of the item moved
68297 * @param {Ext.EventObject} e The event object
68298 */
68299
68300 /**
68301 * @event itemtouchend
68302 * Fires whenever an item is touched
68303 * @param {Ext.dataview.element.Container} this
68304 * @param {Ext.dom.Element} item The item touched
68305 * @param {Number} index The index of the item touched
68306 * @param {Ext.EventObject} e The event object
68307 */
68308
68309 /**
68310 * @event itemtap
68311 * Fires whenever an item is tapped
68312 * @param {Ext.dataview.element.Container} this
68313 * @param {Ext.dom.Element} item The item tapped
68314 * @param {Number} index The index of the item tapped
68315 * @param {Ext.EventObject} e The event object
68316 */
68317
68318 /**
68319 * @event itemtaphold
68320 * Fires whenever an item is tapped
68321 * @param {Ext.dataview.element.Container} this
68322 * @param {Ext.dom.Element} item The item tapped
68323 * @param {Number} index The index of the item tapped
68324 * @param {Ext.EventObject} e The event object
68325 */
68326
68327 /**
68328 * @event itemsingletap
68329 * Fires whenever an item is singletapped
68330 * @param {Ext.dataview.element.Container} this
68331 * @param {Ext.dom.Element} item The item singletapped
68332 * @param {Number} index The index of the item singletapped
68333 * @param {Ext.EventObject} e The event object
68334 */
68335
68336 /**
68337 * @event itemdoubletap
68338 * Fires whenever an item is doubletapped
68339 * @param {Ext.dataview.element.Container} this
68340 * @param {Ext.dom.Element} item The item doubletapped
68341 * @param {Number} index The index of the item doubletapped
68342 * @param {Ext.EventObject} e The event object
68343 */
68344
68345 /**
68346 * @event itemswipe
68347 * Fires whenever an item is swiped
68348 * @param {Ext.dataview.element.Container} this
68349 * @param {Ext.dom.Element} item The item swiped
68350 * @param {Number} index The index of the item swiped
68351 * @param {Ext.EventObject} e The event object
68352 */
68353
68354 doInitialize: function() {
68355 this.element.on({
68356 touchstart: 'onItemTouchStart',
68357 touchend: 'onItemTouchEnd',
68358 tap: 'onItemTap',
68359 taphold: 'onItemTapHold',
68360 touchmove: 'onItemTouchMove',
68361 singletap: 'onItemSingleTap',
68362 doubletap: 'onItemDoubleTap',
68363 swipe: 'onItemSwipe',
68364 delegate: '> div',
68365 scope: this
68366 });
68367 },
68368
68369 //@private
68370 initialize: function() {
68371 this.callParent();
68372 this.doInitialize();
68373 },
68374
68375 updateBaseCls: function(newBaseCls, oldBaseCls) {
68376 var me = this;
68377
68378 me.callParent([newBaseCls + '-container', oldBaseCls]);
68379 },
68380
68381 onItemTouchStart: function(e) {
68382 var me = this,
68383 target = e.getTarget(),
68384 index = me.getViewItems().indexOf(target);
68385
68386 Ext.get(target).on({
68387 touchmove: 'onItemTouchMove',
68388 scope : me,
68389 single: true
68390 });
68391
68392 me.fireEvent('itemtouchstart', me, Ext.get(target), index, e);
68393 },
68394
68395 onItemTouchEnd: function(e) {
68396 var me = this,
68397 target = e.getTarget(),
68398 index = me.getViewItems().indexOf(target);
68399
68400 Ext.get(target).un({
68401 touchmove: 'onItemTouchMove',
68402 scope : me
68403 });
68404
68405 me.fireEvent('itemtouchend', me, Ext.get(target), index, e);
68406 },
68407
68408 onItemTouchMove: function(e) {
68409 var me = this,
68410 target = e.getTarget(),
68411 index = me.getViewItems().indexOf(target);
68412
68413 me.fireEvent('itemtouchmove', me, Ext.get(target), index, e);
68414 },
68415
68416 onItemTap: function(e) {
68417 var me = this,
68418 target = e.getTarget(),
68419 index = me.getViewItems().indexOf(target);
68420
68421 me.fireEvent('itemtap', me, Ext.get(target), index, e);
68422 },
68423
68424 onItemTapHold: function(e) {
68425 var me = this,
68426 target = e.getTarget(),
68427 index = me.getViewItems().indexOf(target);
68428
68429 me.fireEvent('itemtaphold', me, Ext.get(target), index, e);
68430 },
68431
68432 onItemDoubleTap: function(e) {
68433 var me = this,
68434 target = e.getTarget(),
68435 index = me.getViewItems().indexOf(target);
68436
68437 me.fireEvent('itemdoubletap', me, Ext.get(target), index, e);
68438 },
68439
68440 onItemSingleTap: function(e) {
68441 var me = this,
68442 target = e.getTarget(),
68443 index = me.getViewItems().indexOf(target);
68444
68445 me.fireEvent('itemsingletap', me, Ext.get(target), index, e);
68446 },
68447
68448 onItemSwipe: function(e) {
68449 var me = this,
68450 target = e.getTarget(),
68451 index = me.getViewItems().indexOf(target);
68452
68453 me.fireEvent('itemswipe', me, Ext.get(target), index, e);
68454 },
68455
68456 updateListItem: function(record, item) {
68457 var me = this,
68458 dataview = me.dataview,
68459 store = dataview.getStore(),
68460 index = store.indexOf(record),
68461 data = dataview.prepareData(record.getData(true), index, record);
68462
68463 data.xcount = store.getCount();
68464 data.xindex = typeof data.xindex === 'number' ? data.xindex : index;
68465
68466 item.innerHTML = dataview.getItemTpl().apply(data);
68467 },
68468
68469 addListItem: function(index, record) {
68470 var me = this,
68471 dataview = me.dataview,
68472 store = dataview.getStore(),
68473 data = dataview.prepareData(record.getData(true), index, record),
68474 element = me.element,
68475 childNodes = element.dom.childNodes,
68476 ln = childNodes.length,
68477 wrapElement;
68478
68479 data.xcount = typeof data.xcount === 'number' ? data.xcount : store.getCount();
68480 data.xindex = typeof data.xindex === 'number' ? data.xindex : index;
68481
68482 wrapElement = Ext.Element.create(this.getItemElementConfig(index, data));
68483
68484 if (!ln || index == ln) {
68485 wrapElement.appendTo(element);
68486 } else {
68487 wrapElement.insertBefore(childNodes[index]);
68488 }
68489 },
68490
68491 getItemElementConfig: function(index, data) {
68492 var dataview = this.dataview,
68493 itemCls = dataview.getItemCls(),
68494 cls = dataview.getBaseCls() + '-item';
68495
68496 if (itemCls) {
68497 cls += ' ' + itemCls;
68498 }
68499 return {
68500 cls: cls,
68501 html: dataview.getItemTpl().apply(data)
68502 };
68503 },
68504
68505 doRemoveItemCls: function(cls) {
68506 var elements = this.getViewItems(),
68507 ln = elements.length,
68508 i = 0;
68509
68510 for (; i < ln; i++) {
68511 Ext.fly(elements[i]).removeCls(cls);
68512 }
68513 },
68514
68515 doAddItemCls: function(cls) {
68516 var elements = this.getViewItems(),
68517 ln = elements.length,
68518 i = 0;
68519
68520 for (; i < ln; i++) {
68521 Ext.fly(elements[i]).addCls(cls);
68522 }
68523 },
68524
68525 // Remove
68526 moveItemsToCache: function(from, to) {
68527 var me = this,
68528 items = me.getViewItems(),
68529 i = to - from,
68530 item;
68531
68532 for (; i >= 0; i--) {
68533 item = items[from + i];
68534 Ext.get(item).destroy();
68535 }
68536 if (me.getViewItems().length == 0) {
68537 this.dataview.showEmptyText();
68538 }
68539 },
68540
68541 // Add
68542 moveItemsFromCache: function(records) {
68543 var me = this,
68544 dataview = me.dataview,
68545 store = dataview.getStore(),
68546 ln = records.length,
68547 i, record;
68548
68549 if (ln) {
68550 dataview.hideEmptyText();
68551 }
68552
68553 for (i = 0; i < ln; i++) {
68554 records[i]._tmpIndex = store.indexOf(records[i]);
68555 }
68556
68557 Ext.Array.sort(records, function(record1, record2) {
68558 return record1._tmpIndex > record2._tmpIndex ? 1 : -1;
68559 });
68560
68561 for (i = 0; i < ln; i++) {
68562 record = records[i];
68563 me.addListItem(record._tmpIndex, record);
68564 delete record._tmpIndex;
68565 }
68566 },
68567
68568 // Transform ChildNodes into a proper Array so we can do indexOf...
68569 getViewItems: function() {
68570 return Array.prototype.slice.call(this.element.dom.childNodes);
68571 },
68572
68573 updateAtNewIndex: function(oldIndex, newIndex, record) {
68574 this.moveItemsToCache(oldIndex, oldIndex);
68575 this.moveItemsFromCache([record]);
68576 },
68577
68578 destroy: function() {
68579 var elements = this.getViewItems(),
68580 ln = elements.length,
68581 i = 0;
68582
68583 for (; i < ln; i++) {
68584 Ext.get(elements[i]).destroy();
68585 }
68586 this.callParent();
68587 }
68588 });
68589
68590 /**
68591 * DataView makes it easy to create lots of components dynamically, usually based off a {@link Ext.data.Store Store}.
68592 * It's great for rendering lots of data from your server backend or any other data source and is what powers
68593 * components like {@link Ext.List}.
68594 *
68595 * Use DataView whenever you want to show sets of the same component many times, for examples in apps like these:
68596 *
68597 * - List of messages in an email app
68598 * - Showing latest news/tweets
68599 * - Tiled set of albums in an HTML5 music player
68600 *
68601 * # Creating a Simple DataView
68602 *
68603 * At its simplest, a DataView is just a Store full of data and a simple template that we use to render each item:
68604 *
68605 * @example miniphone preview
68606 * var touchTeam = Ext.create('Ext.DataView', {
68607 * fullscreen: true,
68608 * store: {
68609 * fields: ['name', 'age'],
68610 * data: [
68611 * {name: 'Jamie', age: 100},
68612 * {name: 'Rob', age: 21},
68613 * {name: 'Tommy', age: 24},
68614 * {name: 'Jacky', age: 24},
68615 * {name: 'Ed', age: 26}
68616 * ]
68617 * },
68618 *
68619 * itemTpl: '<div>{name} is {age} years old</div>'
68620 * });
68621 *
68622 * Here we just defined everything inline so it's all local with nothing being loaded from a server. For each of the 5
68623 * data items defined in our Store, DataView will render a {@link Ext.Component Component} and pass in the name and age
68624 * data. The component will use the tpl we provided above, rendering the data in the curly bracket placeholders we
68625 * provided.
68626 *
68627 * Because DataView is integrated with Store, any changes to the Store are immediately reflected on the screen. For
68628 * example, if we add a new record to the Store it will be rendered into our DataView:
68629 *
68630 * touchTeam.getStore().add({
68631 * name: 'Abe Elias',
68632 * age: 33
68633 * });
68634 *
68635 * We didn't have to manually update the DataView, it's just automatically updated. The same happens if we modify one
68636 * of the existing records in the Store:
68637 *
68638 * touchTeam.getStore().getAt(0).set('age', 42);
68639 *
68640 * This will get the first record in the Store (Jamie), change the age to 42 and automatically update what's on the
68641 * screen.
68642 *
68643 * @example miniphone
68644 * var touchTeam = Ext.create('Ext.DataView', {
68645 * fullscreen: true,
68646 * store: {
68647 * fields: ['name', 'age'],
68648 * data: [
68649 * {name: 'Jamie', age: 100},
68650 * {name: 'Rob', age: 21},
68651 * {name: 'Tommy', age: 24},
68652 * {name: 'Jacky', age: 24},
68653 * {name: 'Ed', age: 26}
68654 * ]
68655 * },
68656 *
68657 * itemTpl: '<div>{name} is {age} years old</div>'
68658 * });
68659 *
68660 * touchTeam.getStore().add({
68661 * name: 'Abe Elias',
68662 * age: 33
68663 * });
68664 *
68665 * touchTeam.getStore().getAt(0).set('age', 42);
68666 *
68667 * # Loading data from a server
68668 *
68669 * We often want to load data from our server or some other web service so that we don't have to hard code it all
68670 * locally. Let's say we want to load some horror movies from Rotten Tomatoes into a DataView, and for each one
68671 * render the cover image and title. To do this all we have to do is grab an api key from rotten tomatoes (http://developer.rottentomatoes.com/)
68672 * and modify the {@link #store} and {@link #itemTpl} a little:
68673 *
68674 * @example portrait
68675 * Ext.create('Ext.DataView', {
68676 * fullscreen: true,
68677 * store: {
68678 * autoLoad: true,
68679 * fields: ['id', 'title',
68680 * {
68681 * name:'thumbnail_image',
68682 * convert: function(v, record) {return record.raw.posters.thumbnail; }
68683 * }],
68684 *
68685 * proxy: {
68686 * type: 'jsonp',
68687 * // Modify this line with your API key, pretty please...
68688 * url: 'http://api.rottentomatoes.com/api/public/v1.0/movies.json?apikey=hbjgfgryw8tygxztr5wtag3u&q=Horror',
68689 *
68690 * reader: {
68691 * type: 'json',
68692 * rootProperty: 'results'
68693 * }
68694 * }
68695 * },
68696 *
68697 * itemTpl: '<img src="{thumbnail_image}" /><p>{title}</p><div style="clear: both"></div>'
68698 * });
68699 *
68700 * The Store no longer has hard coded data, instead we've provided a {@link Ext.data.proxy.Proxy Proxy}, which fetches
68701 * the data for us. In this case we used a JSON-P proxy so that we can load from Twitter's JSON-P search API. We also
68702 * specified the fields present for each tweet, and used Store's {@link Ext.data.Store#autoLoad autoLoad} configuration
68703 * to load automatically. Finally, we configured a Reader to decode the response from Twitter, telling it to expect
68704 * JSON and that the tweets can be found in the 'results' part of the JSON response.
68705 *
68706 * The last thing we did is update our template to render the image, Twitter username and message. All we need to do
68707 * now is add a little CSS to style the list the way we want it and we end up with a very basic Twitter viewer. Click
68708 * the preview button on the example above to see it in action.
68709 *
68710 * ###Further Reading
68711 * [Sencha Touch DataView Guide](../../../components/dataview.html)
68712 */
68713 Ext.define('Ext.dataview.DataView', {
68714 extend: Ext.Container ,
68715
68716 alternateClassName: 'Ext.DataView',
68717
68718 mixins: [ Ext.mixin.Selectable ],
68719
68720 xtype: 'dataview',
68721
68722
68723
68724
68725
68726
68727
68728
68729 /**
68730 * @event containertap
68731 * Fires when a tap occurs and it is not on a template node.
68732 * @removed 2.0.0
68733 */
68734
68735 /**
68736 * @event itemtouchstart
68737 * Fires whenever an item is touched
68738 * @param {Ext.dataview.DataView} this
68739 * @param {Number} index The index of the item touched
68740 * @param {Ext.Element/Ext.dataview.component.DataItem} target The element or DataItem touched
68741 * @param {Ext.data.Model} record The record associated to the item
68742 * @param {Ext.EventObject} e The event object
68743 */
68744
68745 /**
68746 * @event itemtouchmove
68747 * Fires whenever an item is moved
68748 * @param {Ext.dataview.DataView} this
68749 * @param {Number} index The index of the item moved
68750 * @param {Ext.Element/Ext.dataview.component.DataItem} target The element or DataItem moved
68751 * @param {Ext.data.Model} record The record associated to the item
68752 * @param {Ext.EventObject} e The event object
68753 */
68754
68755 /**
68756 * @event itemtouchend
68757 * Fires whenever an item is touched
68758 * @param {Ext.dataview.DataView} this
68759 * @param {Number} index The index of the item touched
68760 * @param {Ext.Element/Ext.dataview.component.DataItem} target The element or DataItem touched
68761 * @param {Ext.data.Model} record The record associated to the item
68762 * @param {Ext.EventObject} e The event object
68763 */
68764
68765 /**
68766 * @event itemtap
68767 * Fires whenever an item is tapped
68768 * @param {Ext.dataview.DataView} this
68769 * @param {Number} index The index of the item tapped
68770 * @param {Ext.Element/Ext.dataview.component.DataItem} target The element or DataItem tapped
68771 * @param {Ext.data.Model} record The record associated to the item
68772 * @param {Ext.EventObject} e The event object
68773 */
68774
68775 /**
68776 * @event itemtaphold
68777 * Fires whenever an item's taphold event fires
68778 * @param {Ext.dataview.DataView} this
68779 * @param {Number} index The index of the item touched
68780 * @param {Ext.Element/Ext.dataview.component.DataItem} target The element or DataItem touched
68781 * @param {Ext.data.Model} record The record associated to the item
68782 * @param {Ext.EventObject} e The event object
68783 */
68784
68785 /**
68786 * @event itemsingletap
68787 * Fires whenever an item is singletapped
68788 * @param {Ext.dataview.DataView} this
68789 * @param {Number} index The index of the item singletapped
68790 * @param {Ext.Element/Ext.dataview.component.DataItem} target The element or DataItem singletapped
68791 * @param {Ext.data.Model} record The record associated to the item
68792 * @param {Ext.EventObject} e The event object
68793 */
68794
68795 /**
68796 * @event itemdoubletap
68797 * Fires whenever an item is doubletapped
68798 * @param {Ext.dataview.DataView} this
68799 * @param {Number} index The index of the item doubletapped
68800 * @param {Ext.Element/Ext.dataview.component.DataItem} target The element or DataItem doubletapped
68801 * @param {Ext.data.Model} record The record associated to the item
68802 * @param {Ext.EventObject} e The event object
68803 */
68804
68805 /**
68806 * @event itemswipe
68807 * Fires whenever an item is swiped
68808 * @param {Ext.dataview.DataView} this
68809 * @param {Number} index The index of the item swiped
68810 * @param {Ext.Element/Ext.dataview.component.DataItem} target The element or DataItem swiped
68811 * @param {Ext.data.Model} record The record associated to the item
68812 * @param {Ext.EventObject} e The event object
68813 */
68814
68815 /**
68816 * @event select
68817 * @preventable doItemSelect
68818 * Fires whenever an item is selected
68819 * @param {Ext.dataview.DataView} this
68820 * @param {Ext.data.Model} record The record associated to the item
68821 */
68822
68823 /**
68824 * @event deselect
68825 * @preventable doItemDeselect
68826 * Fires whenever an item is deselected
68827 * @param {Ext.dataview.DataView} this
68828 * @param {Ext.data.Model} record The record associated to the item
68829 * @param {Boolean} supressed Flag to suppress the event
68830 */
68831
68832 /**
68833 * @event refresh
68834 * @preventable doRefresh
68835 * Fires whenever the DataView is refreshed
68836 * @param {Ext.dataview.DataView} this
68837 */
68838
68839 /**
68840 * @hide
68841 * @event add
68842 */
68843
68844 /**
68845 * @hide
68846 * @event remove
68847 */
68848
68849 /**
68850 * @hide
68851 * @event move
68852 */
68853
68854 config: {
68855 /**
68856 * @cfg layout
68857 * Hide layout config in DataView. It only causes confusion.
68858 * @accessor
68859 * @private
68860 */
68861
68862 /**
68863 * @cfg {Ext.data.Store/Object} store
68864 * Can be either a Store instance or a configuration object that will be turned into a Store. The Store is used
68865 * to populate the set of items that will be rendered in the DataView. See the DataView intro documentation for
68866 * more information about the relationship between Store and DataView.
68867 * @accessor
68868 */
68869 store: null,
68870
68871 /**
68872 * @cfg {Object[]} data
68873 * @inheritdoc
68874 */
68875 data: null,
68876
68877 /**
68878 * @cfg baseCls
68879 * @inheritdoc
68880 */
68881 baseCls: Ext.baseCSSPrefix + 'dataview',
68882
68883 /**
68884 * @cfg {String} emptyText
68885 * The text to display in the view when there is no data to display
68886 */
68887 emptyText: null,
68888
68889 /**
68890 * @cfg {Boolean} deferEmptyText `true` to defer `emptyText` being applied until the store's first load.
68891 */
68892 deferEmptyText: true,
68893
68894 /**
68895 * @cfg {String/String[]/Ext.XTemplate} itemTpl
68896 * The `tpl` to use for each of the items displayed in this DataView.
68897 */
68898 itemTpl: '<div>{text}</div>',
68899
68900 /**
68901 * @cfg {String} pressedCls
68902 * The CSS class to apply to an item on the view while it is being pressed.
68903 * @accessor
68904 */
68905 pressedCls: 'x-item-pressed',
68906
68907 /**
68908 * @cfg {String} itemCls
68909 * An additional CSS class to apply to items within the DataView.
68910 * @accessor
68911 */
68912 itemCls: null,
68913
68914 /**
68915 * @cfg {String} selectedCls
68916 * The CSS class to apply to an item on the view while it is selected.
68917 * @accessor
68918 */
68919 selectedCls: 'x-item-selected',
68920
68921 /**
68922 * @cfg {String} triggerEvent
68923 * Determines what type of touch event causes an item to be selected.
68924 * Valid options are: 'itemtap', 'itemsingletap', 'itemdoubletap', 'itemswipe', 'itemtaphold'.
68925 * @accessor
68926 */
68927 triggerEvent: 'itemtap',
68928
68929 /**
68930 * @cfg {String} triggerCtEvent
68931 * Determines what type of touch event is recognized as a touch on the container.
68932 * Valid options are 'tap' and 'singletap'.
68933 * @accessor
68934 */
68935 triggerCtEvent: 'tap',
68936
68937 /**
68938 * @cfg {Boolean} deselectOnContainerClick
68939 * When set to true, tapping on the DataView's background (i.e. not on
68940 * an item in the DataView) will deselect any currently selected items.
68941 * @accessor
68942 */
68943 deselectOnContainerClick: true,
68944
68945 /**
68946 * @cfg scrollable
68947 * @inheritdoc
68948 */
68949 scrollable: true,
68950
68951 /**
68952 * @cfg {Boolean/Object} inline
68953 * When set to `true` the items within the DataView will have their display set to inline-block
68954 * and be arranged horizontally. By default the items will wrap to the width of the DataView.
68955 * Passing an object with `{ wrap: false }` will turn off this wrapping behavior and overflowed
68956 * items will need to be scrolled to horizontally.
68957 * @accessor
68958 */
68959 inline: null,
68960
68961 /**
68962 * @cfg {Number} pressedDelay
68963 * The amount of delay between the `tapstart` and the moment we add the `pressedCls`.
68964 *
68965 * Settings this to `true` defaults to 100ms.
68966 * @accessor
68967 */
68968 pressedDelay: 100,
68969
68970 /**
68971 * @cfg {String/Boolean} loadingText
68972 * A string to display during data load operations. If specified, this text will be
68973 * displayed in a loading div and the view's contents will be cleared while loading, otherwise the view's
68974 * contents will continue to display normally until the new data is loaded and the contents are replaced.
68975 */
68976 loadingText: 'Loading...',
68977
68978 /**
68979 * @cfg {Boolean} useComponents
68980 * Flag the use a component based DataView implementation. This allows the full use of components in the
68981 * DataView at the cost of some performance.
68982 *
68983 * Checkout the [Sencha Touch DataView Guide](../../../components/dataview.html) for more information on using this configuration.
68984 * @accessor
68985 */
68986 useComponents: null,
68987
68988 /**
68989 * @cfg {Object} itemConfig
68990 * A configuration object that is passed to every item created by a component based DataView. Because each
68991 * item that a DataView renders is a Component, we can pass configuration options to each component to
68992 * easily customize how each child component behaves.
68993 *
68994 * __Note:__ this is only used when `{@link #useComponents}` is `true`.
68995 * @accessor
68996 */
68997 itemConfig: {},
68998
68999 /**
69000 * @cfg {Number} maxItemCache
69001 * Maintains a cache of reusable components when using a component based DataView. Improving performance at
69002 * the cost of memory.
69003 *
69004 * __Note:__ this is currently only used when `{@link #useComponents}` is `true`.
69005 * @accessor
69006 */
69007 maxItemCache: 20,
69008
69009 /**
69010 * @cfg {String} defaultType
69011 * The xtype used for the component based DataView.
69012 *
69013 * __Note:__ this is only used when `{@link #useComponents}` is `true`.
69014 * @accessor
69015 */
69016 defaultType: 'dataitem',
69017
69018 /**
69019 * @cfg {Boolean} scrollToTopOnRefresh
69020 * Scroll the DataView to the top when the DataView is refreshed.
69021 * @accessor
69022 */
69023 scrollToTopOnRefresh: true
69024 },
69025
69026 constructor: function(config) {
69027 var me = this,
69028 layout;
69029
69030 me.hasLoadedStore = false;
69031
69032 me.mixins.selectable.constructor.apply(me, arguments);
69033
69034 me.indexOffset = 0;
69035
69036 me.callParent(arguments);
69037
69038 //<debug>
69039 layout = this.getLayout();
69040 if (layout && !layout.isAuto) {
69041 Ext.Logger.error('The base layout for a DataView must always be an Auto Layout');
69042 }
69043 //</debug>
69044 },
69045
69046 updateItemCls: function(newCls, oldCls) {
69047 var container = this.container;
69048 if (container) {
69049 if (oldCls) {
69050 container.doRemoveItemCls(oldCls);
69051 }
69052 if (newCls) {
69053 container.doAddItemCls(newCls);
69054 }
69055 }
69056 },
69057
69058 storeEventHooks: {
69059 beforeload: 'onBeforeLoad',
69060 load: 'onLoad',
69061 refresh: 'refresh',
69062 addrecords: 'onStoreAdd',
69063 removerecords: 'onStoreRemove',
69064 updaterecord: 'onStoreUpdate'
69065 },
69066
69067 initialize: function() {
69068 this.callParent();
69069 var me = this,
69070 container,
69071 triggerEvent = me.getTriggerEvent();
69072
69073 me.on(me.getTriggerCtEvent(), me.onContainerTrigger, me);
69074
69075 container = me.container = this.add(new Ext.dataview[me.getUseComponents() ? 'component' : 'element'].Container({
69076 baseCls: this.getBaseCls()
69077 }));
69078 container.dataview = me;
69079
69080 if (triggerEvent) {
69081 me.on(triggerEvent, me.onItemTrigger, me);
69082 }
69083
69084 container.on({
69085 itemtouchstart: 'onItemTouchStart',
69086 itemtouchend: 'onItemTouchEnd',
69087 itemtap: 'onItemTap',
69088 itemtaphold: 'onItemTapHold',
69089 itemtouchmove: 'onItemTouchMove',
69090 itemsingletap: 'onItemSingleTap',
69091 itemdoubletap: 'onItemDoubleTap',
69092 itemswipe: 'onItemSwipe',
69093 scope: me
69094 });
69095
69096 if (me.getStore()) {
69097 if (me.isPainted()) {
69098 me.refresh();
69099 }
69100 else {
69101 me.on({
69102 painted: 'refresh',
69103 single: true
69104 });
69105 }
69106 }
69107 },
69108
69109 applyInline: function(config) {
69110 if (Ext.isObject(config)) {
69111 config = Ext.apply({}, config);
69112 }
69113 return config;
69114 },
69115
69116 updateInline: function(newInline, oldInline) {
69117 var baseCls = this.getBaseCls();
69118 if (oldInline) {
69119 this.removeCls([baseCls + '-inlineblock', baseCls + '-nowrap']);
69120 }
69121 if (newInline) {
69122 this.addCls(baseCls + '-inlineblock');
69123 if (Ext.isObject(newInline) && newInline.wrap === false) {
69124 this.addCls(baseCls + '-nowrap');
69125 }
69126 else {
69127 this.removeCls(baseCls + '-nowrap');
69128 }
69129 }
69130 },
69131
69132 /**
69133 * Function which can be overridden to provide custom formatting for each Record that is used by this
69134 * DataView's {@link #tpl template} to render each node.
69135 * @param {Object/Object[]} data The raw data object that was used to create the Record.
69136 * @param {Number} index the index number of the Record being prepared for rendering.
69137 * @param {Ext.data.Model} record The Record being prepared for rendering.
69138 * @return {Array/Object} The formatted data in a format expected by the internal {@link #tpl template}'s `overwrite()` method.
69139 * (either an array if your params are numeric (i.e. `{0}`) or an object (i.e. `{foo: 'bar'}`))
69140 */
69141 prepareData: function(data, index, record) {
69142 return data;
69143 },
69144
69145 // apply to the selection model to maintain visual UI cues
69146 onContainerTrigger: function(e) {
69147 var me = this;
69148 if (e.target != me.element.dom) {
69149 return;
69150 }
69151 if (me.getDeselectOnContainerClick() && me.getStore()) {
69152 me.deselectAll();
69153 }
69154 },
69155
69156 // apply to the selection model to maintain visual UI cues
69157 onItemTrigger: function(me, index) {
69158 if (!this.isDestroyed) {
69159 this.selectWithEvent(this.getStore().getAt(index));
69160 }
69161 },
69162
69163 doAddPressedCls: function(record) {
69164 var me = this,
69165 item = me.getItemAt(me.getStore().indexOf(record));
69166 if (Ext.isElement(item)) {
69167 item = Ext.get(item);
69168 }
69169 if (item) {
69170 if (item.isComponent) {
69171 item.renderElement.addCls(me.getPressedCls());
69172 } else {
69173 item.addCls(me.getPressedCls());
69174 }
69175 }
69176 },
69177
69178 onItemTouchStart: function(container, target, index, e) {
69179 var me = this,
69180 store = me.getStore(),
69181 record = store && store.getAt(index);
69182
69183 me.fireAction('itemtouchstart', [me, index, target, record, e], 'doItemTouchStart');
69184 },
69185
69186 doItemTouchStart: function(me, index, target, record) {
69187 var pressedDelay = me.getPressedDelay();
69188
69189 if (record) {
69190 if (pressedDelay > 0) {
69191 me.pressedTimeout = Ext.defer(me.doAddPressedCls, pressedDelay, me, [record]);
69192 }
69193 else {
69194 me.doAddPressedCls(record);
69195 }
69196 }
69197 },
69198
69199 onItemTouchEnd: function(container, target, index, e) {
69200 var me = this,
69201 store = me.getStore(),
69202 record = store && store.getAt(index);
69203
69204 if (this.hasOwnProperty('pressedTimeout')) {
69205 clearTimeout(this.pressedTimeout);
69206 delete this.pressedTimeout;
69207 }
69208
69209 if (record && target) {
69210 if (target.isComponent) {
69211 target.renderElement.removeCls(me.getPressedCls());
69212 } else {
69213 target.removeCls(me.getPressedCls());
69214 }
69215 }
69216
69217 me.fireEvent('itemtouchend', me, index, target, record, e);
69218 },
69219
69220 onItemTouchMove: function(container, target, index, e) {
69221 var me = this,
69222 store = me.getStore(),
69223 record = store && store.getAt(index);
69224
69225 if (me.hasOwnProperty('pressedTimeout')) {
69226 clearTimeout(me.pressedTimeout);
69227 delete me.pressedTimeout;
69228 }
69229
69230 if (record && target) {
69231 if (target.isComponent) {
69232 target.renderElement.removeCls(me.getPressedCls());
69233 } else {
69234 target.removeCls(me.getPressedCls());
69235 }
69236 }
69237 me.fireEvent('itemtouchmove', me, index, target, record, e);
69238 },
69239
69240 onItemTap: function(container, target, index, e) {
69241 var me = this,
69242 store = me.getStore(),
69243 record = store && store.getAt(index);
69244
69245 me.fireEvent('itemtap', me, index, target, record, e);
69246 },
69247
69248 onItemTapHold: function(container, target, index, e) {
69249 var me = this,
69250 store = me.getStore(),
69251 record = store && store.getAt(index);
69252
69253 me.fireEvent('itemtaphold', me, index, target, record, e);
69254 },
69255
69256 onItemSingleTap: function(container, target, index, e) {
69257 var me = this,
69258 store = me.getStore(),
69259 record = store && store.getAt(index);
69260
69261 me.fireEvent('itemsingletap', me, index, target, record, e);
69262 },
69263
69264 onItemDoubleTap: function(container, target, index, e) {
69265 var me = this,
69266 store = me.getStore(),
69267 record = store && store.getAt(index);
69268
69269 me.fireEvent('itemdoubletap', me, index, target, record, e);
69270 },
69271
69272 onItemSwipe: function(container, target, index, e) {
69273 var me = this,
69274 store = me.getStore(),
69275 record = store && store.getAt(index);
69276
69277 me.fireEvent('itemswipe', me, index, target, record, e);
69278 },
69279
69280 // invoked by the selection model to maintain visual UI cues
69281 onItemSelect: function(record, suppressEvent) {
69282 var me = this;
69283 if (suppressEvent) {
69284 me.doItemSelect(me, record);
69285 } else {
69286 me.fireAction('select', [me, record], 'doItemSelect');
69287 }
69288 },
69289
69290 // invoked by the selection model to maintain visual UI cues
69291 doItemSelect: function(me, record) {
69292 if (me.container && !me.isDestroyed) {
69293 var item = me.getItemAt(me.getStore().indexOf(record));
69294 if (Ext.isElement(item)) {
69295 item = Ext.get(item);
69296 }
69297 if (item) {
69298 if (item.isComponent) {
69299 item.renderElement.removeCls(me.getPressedCls());
69300 item.renderElement.addCls(me.getSelectedCls());
69301 } else {
69302 item.removeCls(me.getPressedCls());
69303 item.addCls(me.getSelectedCls());
69304 }
69305 }
69306 }
69307 },
69308
69309 // invoked by the selection model to maintain visual UI cues
69310 onItemDeselect: function(record, suppressEvent) {
69311 var me = this;
69312 if (me.container && !me.isDestroyed) {
69313 if (suppressEvent) {
69314 me.doItemDeselect(me, record);
69315 }
69316 else {
69317 me.fireAction('deselect', [me, record, suppressEvent], 'doItemDeselect');
69318 }
69319 }
69320 },
69321
69322 doItemDeselect: function(me, record) {
69323 var item = me.getItemAt(me.getStore().indexOf(record));
69324
69325 if (Ext.isElement(item)) {
69326 item = Ext.get(item);
69327 }
69328
69329 if (item) {
69330 if (item.isComponent) {
69331 item.renderElement.removeCls([me.getPressedCls(), me.getSelectedCls()]);
69332 } else {
69333 item.removeCls([me.getPressedCls(), me.getSelectedCls()]);
69334 }
69335 }
69336 },
69337
69338 updateData: function(data) {
69339 var store = this.getStore();
69340 if (!store) {
69341 this.setStore(Ext.create('Ext.data.Store', {
69342 data: data,
69343 autoDestroy: true
69344 }));
69345 } else {
69346 store.add(data);
69347 }
69348 },
69349
69350 applyStore: function(store) {
69351 var me = this,
69352 bindEvents = Ext.apply({}, me.storeEventHooks, { scope: me }),
69353 proxy, reader;
69354
69355 if (store) {
69356 store = Ext.data.StoreManager.lookup(store);
69357 if (store && Ext.isObject(store) && store.isStore) {
69358 store.on(bindEvents);
69359 proxy = store.getProxy();
69360 if (proxy) {
69361 reader = proxy.getReader();
69362 if (reader) {
69363 reader.on('exception', 'handleException', this);
69364 }
69365 }
69366 }
69367 //<debug warn>
69368 else {
69369 Ext.Logger.warn("The specified Store cannot be found", this);
69370 }
69371 //</debug>
69372 }
69373
69374 return store;
69375 },
69376
69377 /**
69378 * Method called when the Store's Reader throws an exception
69379 * @method handleException
69380 */
69381 handleException: function() {
69382 this.setMasked(false);
69383 },
69384
69385 updateStore: function(newStore, oldStore) {
69386 var me = this,
69387 bindEvents = Ext.apply({}, me.storeEventHooks, { scope: me }),
69388 proxy, reader;
69389
69390 if (oldStore && Ext.isObject(oldStore) && oldStore.isStore) {
69391 oldStore.un(bindEvents);
69392
69393 if (!me.isDestroyed) {
69394 me.onStoreClear();
69395 }
69396
69397 if (oldStore.getAutoDestroy()) {
69398 oldStore.destroy();
69399 }
69400 else {
69401 proxy = oldStore.getProxy();
69402 if (proxy) {
69403 reader = proxy.getReader();
69404 if (reader) {
69405 reader.un('exception', 'handleException', this);
69406 }
69407 }
69408 }
69409 }
69410
69411 if (newStore) {
69412 if (newStore.isLoaded()) {
69413 this.hasLoadedStore = true;
69414 }
69415
69416 if (newStore.isLoading()) {
69417 me.onBeforeLoad();
69418 }
69419 if (me.container) {
69420 me.refresh();
69421 }
69422 }
69423 },
69424
69425 onBeforeLoad: function() {
69426 var loadingText = this.getLoadingText();
69427 if (loadingText && this.isPainted()) {
69428 this.setMasked({
69429 xtype: 'loadmask',
69430 message: loadingText
69431 });
69432 }
69433
69434 this.hideEmptyText();
69435 },
69436
69437 updateEmptyText: function(newEmptyText, oldEmptyText) {
69438 var me = this,
69439 store;
69440
69441 if (oldEmptyText && me.emptyTextCmp) {
69442 me.remove(me.emptyTextCmp, true);
69443 delete me.emptyTextCmp;
69444 }
69445
69446 if (newEmptyText) {
69447 me.emptyTextCmp = me.add({
69448 xtype: 'component',
69449 cls: me.getBaseCls() + '-emptytext',
69450 html: newEmptyText,
69451 hidden: true
69452 });
69453 store = me.getStore();
69454 if (store && me.hasLoadedStore && !store.getCount()) {
69455 this.showEmptyText();
69456 }
69457 }
69458 },
69459
69460 onLoad: function(store) {
69461 //remove any masks on the store
69462 this.hasLoadedStore = true;
69463 this.setMasked(false);
69464
69465 if (!store.getCount()) {
69466 this.showEmptyText();
69467 }
69468 },
69469
69470 /**
69471 * Refreshes the view by reloading the data from the store and re-rendering the template.
69472 */
69473 refresh: function() {
69474 var me = this,
69475 container = me.container;
69476
69477 if (!me.getStore()) {
69478 if (!me.hasLoadedStore && !me.getDeferEmptyText()) {
69479 me.showEmptyText();
69480 }
69481 return;
69482 }
69483 if (container) {
69484 me.fireAction('refresh', [me], 'doRefresh');
69485 }
69486 },
69487
69488 applyItemTpl: function(config) {
69489 return (Ext.isObject(config) && config.isTemplate) ? config : new Ext.XTemplate(config);
69490 },
69491
69492 onAfterRender: function() {
69493 var me = this;
69494 me.callParent(arguments);
69495 me.updateStore(me.getStore());
69496 },
69497
69498 /**
69499 * Returns an item at the specified index.
69500 * @param {Number} index Index of the item.
69501 * @return {Ext.dom.Element/Ext.dataview.component.DataItem} item Item at the specified index.
69502 */
69503 getItemAt: function(index) {
69504 return this.getViewItems()[index - this.indexOffset];
69505 },
69506
69507 /**
69508 * Returns an index for the specified item.
69509 * @param {Number} item The item to locate.
69510 * @return {Number} Index for the specified item.
69511 */
69512 getItemIndex: function(item) {
69513 var index = this.getViewItems().indexOf(item);
69514 return (index === -1) ? index : this.indexOffset + index;
69515 },
69516
69517 /**
69518 * Returns an array of the current items in the DataView.
69519 * @return {Ext.dom.Element[]/Ext.dataview.component.DataItem[]} Array of Items.
69520 */
69521 getViewItems: function() {
69522 return this.container.getViewItems();
69523 },
69524
69525 doRefresh: function(me) {
69526 var container = me.container,
69527 store = me.getStore(),
69528 records = store.getRange(),
69529 items = me.getViewItems(),
69530 recordsLn = records.length,
69531 itemsLn = items.length,
69532 deltaLn = recordsLn - itemsLn,
69533 scrollable = me.getScrollable(),
69534 i, item;
69535
69536 if (this.getScrollToTopOnRefresh() && scrollable) {
69537 scrollable.getScroller().scrollToTop();
69538 }
69539
69540 // No items, hide all the items from the collection.
69541 if (recordsLn < 1) {
69542 me.onStoreClear();
69543 return;
69544 } else {
69545 me.hideEmptyText();
69546 }
69547
69548 // Too many items, hide the unused ones
69549 if (deltaLn < 0) {
69550 container.moveItemsToCache(itemsLn + deltaLn, itemsLn - 1);
69551 // Items can changed, we need to refresh our references
69552 items = me.getViewItems();
69553 itemsLn = items.length;
69554 }
69555 // Not enough items, create new ones
69556 else if (deltaLn > 0) {
69557 container.moveItemsFromCache(store.getRange(itemsLn));
69558 }
69559
69560 // Update Data and insert the new html for existing items
69561 for (i = 0; i < itemsLn; i++) {
69562 item = items[i];
69563 container.updateListItem(records[i], item);
69564 }
69565
69566 if (this.hasSelection()) {
69567 var selection = this.getSelection(),
69568 selectionLn = this.getSelectionCount(),
69569 record;
69570 for (i = 0; i < selectionLn; i++) {
69571 record = selection[i];
69572 this.doItemSelect(this, record);
69573 }
69574 }
69575 },
69576
69577 showEmptyText: function() {
69578 if (this.getEmptyText() && (this.hasLoadedStore || !this.getDeferEmptyText())) {
69579 this.emptyTextCmp.show();
69580 }
69581 },
69582
69583 hideEmptyText: function() {
69584 if (this.getEmptyText()) {
69585 this.emptyTextCmp.hide();
69586 }
69587 },
69588
69589 destroy: function() {
69590 var store = this.getStore(),
69591 proxy = (store && store.getProxy()),
69592 reader = (proxy && proxy.getReader());
69593
69594 if (reader) {
69595 // TODO: Use un() instead of clearListeners() when TOUCH-2723 is fixed.
69596 // reader.un('exception', 'handleException', this);
69597 reader.clearListeners();
69598 }
69599
69600 this.callParent(arguments);
69601
69602 this.setStore(null);
69603 },
69604
69605 onStoreClear: function() {
69606 var me = this,
69607 container = me.container,
69608 items = me.getViewItems();
69609
69610 container.moveItemsToCache(0, items.length - 1);
69611 this.showEmptyText();
69612 },
69613
69614 /**
69615 * @private
69616 * @param {Ext.data.Store} store
69617 * @param {Array} records
69618 */
69619 onStoreAdd: function(store, records) {
69620 if (records) {
69621 this.hideEmptyText();
69622 this.container.moveItemsFromCache(records);
69623 }
69624 },
69625
69626 /**
69627 * @private
69628 * @param {Ext.data.Store} store
69629 * @param {Array} records
69630 * @param {Array} indices
69631 */
69632 onStoreRemove: function(store, records, indices) {
69633 var container = this.container,
69634 ln = records.length,
69635 i;
69636 for (i = 0; i < ln; i++) {
69637 container.moveItemsToCache(indices[i], indices[i]);
69638 }
69639 },
69640
69641 /**
69642 * @private
69643 * @param {Ext.data.Store} store
69644 * @param {Ext.data.Model} record
69645 * @param {Number} newIndex
69646 * @param {Number} oldIndex
69647 */
69648 onStoreUpdate: function(store, record, newIndex, oldIndex) {
69649 var me = this,
69650 container = me.container,
69651 item;
69652
69653 oldIndex = (typeof oldIndex === 'undefined') ? newIndex : oldIndex;
69654
69655 if (oldIndex !== newIndex) {
69656 container.updateAtNewIndex(oldIndex, newIndex, record);
69657 if (me.isSelected(record)) {
69658 me.doItemSelect(me, record);
69659 }
69660 }
69661 else {
69662 item = me.getViewItems()[newIndex];
69663 if (item) {
69664 // Bypassing setter because sometimes we pass the same record (different data)
69665 container.updateListItem(record, item);
69666 }
69667 }
69668 }
69669 });
69670
69671 /**
69672 * @class Ext.chart.Legend
69673 * @extends Ext.dataview.DataView
69674 *
69675 * A default legend for charts.
69676 *
69677 * @example preview
69678 * var chart = new Ext.chart.Chart({
69679 * animate: true,
69680 * store: {
69681 * fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
69682 * data: [
69683 * {'name':'metric one', 'data1':10, 'data2':12, 'data3':14, 'data4':8, 'data5':13},
69684 * {'name':'metric two', 'data1':7, 'data2':8, 'data3':16, 'data4':10, 'data5':3},
69685 * {'name':'metric three', 'data1':5, 'data2':2, 'data3':14, 'data4':12, 'data5':7},
69686 * {'name':'metric four', 'data1':2, 'data2':14, 'data3':6, 'data4':1, 'data5':23},
69687 * {'name':'metric five', 'data1':27, 'data2':38, 'data3':36, 'data4':13, 'data5':33}
69688 * ]
69689 * },
69690 * legend: {
69691 * position: 'bottom'
69692 * },
69693 * axes: [{
69694 * type: 'numeric',
69695 * position: 'left',
69696 * fields: ['data1'],
69697 * title: {
69698 * text: 'Sample Values',
69699 * fontSize: 15
69700 * },
69701 * grid: true,
69702 * minimum: 0
69703 * }, {
69704 * type: 'category',
69705 * position: 'bottom',
69706 * fields: ['name'],
69707 * title: {
69708 * text: 'Sample Values',
69709 * fontSize: 15
69710 * }
69711 * }],
69712 * series: [{
69713 * type: 'area',
69714 * title: ['Data1', 'Data2', 'Data3'],
69715 * subStyle: {
69716 * fill: ['blue', 'green', 'red']
69717 * },
69718 * xField: 'name',
69719 * yField: ['data1', 'data2', 'data3']
69720 *
69721 * }]
69722 * });
69723 * Ext.Viewport.setLayout('fit');
69724 * Ext.Viewport.add(chart);
69725 */
69726 Ext.define("Ext.chart.Legend", {
69727 xtype: 'legend',
69728 extend: Ext.dataview.DataView ,
69729 config: {
69730 itemTpl: [
69731 "<span class=\"x-legend-item-marker {[values.disabled?\'x-legend-inactive\':\'\']}\" style=\"background:{mark};\"></span>{name}"
69732 ],
69733 baseCls: 'x-legend',
69734 padding: 5,
69735 disableSelection: true,
69736 inline: true,
69737 /**
69738 * @cfg {String} position
69739 * @deprecated Use `docked` instead.
69740 * Delegates to `docked`
69741 */
69742 position: null,
69743 /**
69744 * @cfg {Boolean} toggleable 'true' if the series items in the legend can be toggled on and off.
69745 */
69746 toggleable: true,
69747 docked: 'top',
69748 horizontalHeight: 48,
69749 verticalWidth: 150
69750 },
69751
69752 constructor: function () {
69753 this.callSuper(arguments);
69754
69755 var scroller = this.getScrollable().getScroller(),
69756 onDrag = scroller.onDrag;
69757 scroller.onDrag = function (e) {
69758 e.stopPropagation();
69759 onDrag.call(this, e);
69760 };
69761 },
69762
69763 doSetDocked: function (docked) {
69764 this.callSuper(arguments);
69765 if (docked === 'top' || docked === 'bottom') {
69766 this.setLayout({type: 'hbox', pack: 'center'});
69767 this.setInline(true);
69768 // TODO: Remove this when possible
69769 this.setWidth(null);
69770 this.setHeight(this.getHorizontalHeight());
69771 if (this.getScrollable()) {
69772 this.setScrollable({direction: 'horizontal'});
69773 }
69774 } else {
69775 this.setLayout({pack: 'center'});
69776 this.setInline(false);
69777 // TODO: Remove this when possible
69778 this.setWidth(this.getVerticalWidth());
69779 this.setHeight(null);
69780 if (this.getScrollable()) {
69781 this.setScrollable({direction: 'vertical'});
69782 }
69783 }
69784 },
69785
69786 setScrollable: function (scrollable) {
69787 this.callSuper(arguments);
69788 if (scrollable === true) {
69789 if (this.getDocked() === 'top' || this.getDocked() === 'bottom') {
69790 this.setScrollable({direction: 'horizontal'});
69791 } else if (this.getDocked() === 'left' || this.getDocked() === 'right') {
69792 this.setScrollable({direction: 'vertical'});
69793 }
69794 }
69795 },
69796
69797
69798 setPosition: function (position) {
69799 this.setDocked(position);
69800 },
69801
69802 getPosition: function () {
69803 return this.getDocked();
69804 },
69805
69806 onItemTap: function (container, target, index, e) {
69807 this.callSuper(arguments);
69808 if(this.getToggleable()) {
69809 var me = this,
69810 store = me.getStore(),
69811 record = store && store.getAt(index);
69812 record.beginEdit();
69813 record.set('disabled', !record.get('disabled'));
69814 record.commit();
69815 }
69816 }
69817 });
69818
69819 /**
69820 * @class Ext.data.SortTypes
69821 * This class defines a series of static methods that are used on a
69822 * {@link Ext.data.Field} for performing sorting. The methods cast the
69823 * underlying values into a data type that is appropriate for sorting on
69824 * that particular field. If a {@link Ext.data.Field#type} is specified,
69825 * the `sortType` will be set to a sane default if the `sortType` is not
69826 * explicitly defined on the field. The `sortType` will make any necessary
69827 * modifications to the value and return it.
69828 *
69829 * - `asText` - Removes any tags and converts the value to a string.
69830 * - `asUCText` - Removes any tags and converts the value to an uppercase string.
69831 * - `asUCString` - Converts the value to an uppercase string.
69832 * - `asDate` - Converts the value into Unix epoch time.
69833 * - `asFloat` - Converts the value to a floating point number.
69834 * - `asInt` - Converts the value to an integer number.
69835 *
69836 * It is also possible to create a custom `sortType` that can be used throughout
69837 * an application.
69838 *
69839 * Ext.apply(Ext.data.SortTypes, {
69840 * asPerson: function(person){
69841 * // expects an object with a first and last name property
69842 * return person.lastName.toUpperCase() + person.firstName.toLowerCase();
69843 * }
69844 * });
69845 *
69846 * Ext.define('Employee', {
69847 * extend: 'Ext.data.Model',
69848 * config: {
69849 * fields: [{
69850 * name: 'person',
69851 * sortType: 'asPerson'
69852 * }, {
69853 * name: 'salary',
69854 * type: 'float' // sortType set to asFloat
69855 * }]
69856 * }
69857 * });
69858 *
69859 * @singleton
69860 * @docauthor Evan Trimboli <evan@sencha.com>
69861 */
69862 Ext.define('Ext.data.SortTypes', {
69863 singleton: true,
69864
69865 /**
69866 * The regular expression used to strip tags.
69867 * @type {RegExp}
69868 * @property
69869 */
69870 stripTagsRE : /<\/?[^>]+>/gi,
69871
69872 /**
69873 * Default sort that does nothing.
69874 * @param {Object} value The value being converted.
69875 * @return {Object} The comparison value.
69876 */
69877 none : function(value) {
69878 return value;
69879 },
69880
69881 /**
69882 * Strips all HTML tags to sort on text only.
69883 * @param {Object} value The value being converted.
69884 * @return {String} The comparison value.
69885 */
69886 asText : function(value) {
69887 return String(value).replace(this.stripTagsRE, "");
69888 },
69889
69890 /**
69891 * Strips all HTML tags to sort on text only - case insensitive.
69892 * @param {Object} value The value being converted.
69893 * @return {String} The comparison value.
69894 */
69895 asUCText : function(value) {
69896 return String(value).toUpperCase().replace(this.stripTagsRE, "");
69897 },
69898
69899 /**
69900 * Case insensitive string.
69901 * @param {Object} value The value being converted.
69902 * @return {String} The comparison value.
69903 */
69904 asUCString : function(value) {
69905 return String(value).toUpperCase();
69906 },
69907
69908 /**
69909 * Date sorting.
69910 * @param {Object} value The value being converted.
69911 * @return {Number} The comparison value.
69912 */
69913 asDate : function(value) {
69914 if (!value) {
69915 return 0;
69916 }
69917 if (Ext.isDate(value)) {
69918 return value.getTime();
69919 }
69920 return Date.parse(String(value));
69921 },
69922
69923 /**
69924 * Float sorting.
69925 * @param {Object} value The value being converted.
69926 * @return {Number} The comparison value.
69927 */
69928 asFloat : function(value) {
69929 value = parseFloat(String(value).replace(/,/g, ""));
69930 return isNaN(value) ? 0 : value;
69931 },
69932
69933 /**
69934 * Integer sorting.
69935 * @param {Object} value The value being converted.
69936 * @return {Number} The comparison value.
69937 */
69938 asInt : function(value) {
69939 value = parseInt(String(value).replace(/,/g, ""), 10);
69940 return isNaN(value) ? 0 : value;
69941 }
69942 });
69943
69944 /**
69945 * @class Ext.data.Types
69946 *
69947 * This is a static class containing the system-supplied data types which may be given to a {@link Ext.data.Field Field}.
69948 *
69949 * The properties in this class are used as type indicators in the {@link Ext.data.Field Field} class, so to
69950 * test whether a Field is of a certain type, compare the {@link Ext.data.Field#type type} property against properties
69951 * of this class.
69952 *
69953 * Developers may add their own application-specific data types to this class. Definition names must be UPPERCASE.
69954 * each type definition must contain three properties:
69955 *
69956 * - `convert`: {Function} - A function to convert raw data values from a data block into the data
69957 * to be stored in the Field. The function is passed the following parameters:
69958 * + `v`: {Mixed} - The data value as read by the Reader, if `undefined` will use
69959 * the configured `{@link Ext.data.Field#defaultValue defaultValue}`.
69960 * + `rec`: {Mixed} - The data object containing the row as read by the Reader.
69961 * Depending on the Reader type, this could be an Array ({@link Ext.data.reader.Array ArrayReader}), an object
69962 * ({@link Ext.data.reader.Json JsonReader}), or an XML element.
69963 * - `sortType`: {Function} - A function to convert the stored data into comparable form, as defined by {@link Ext.data.SortTypes}.
69964 * - `type`: {String} - A textual data type name.
69965 *
69966 * For example, to create a VELatLong field (See the Microsoft Bing Mapping API) containing the latitude/longitude value of a datapoint on a map from a JsonReader data block
69967 * which contained the properties `lat` and `long`, you would define a new data type like this:
69968 *
69969 * // Add a new Field data type which stores a VELatLong object in the Record.
69970 * Ext.data.Types.VELATLONG = {
69971 * convert: function(v, data) {
69972 * return new VELatLong(data.lat, data.long);
69973 * },
69974 * sortType: function(v) {
69975 * return v.Latitude; // When sorting, order by latitude
69976 * },
69977 * type: 'VELatLong'
69978 * };
69979 *
69980 * Then, when declaring a Model, use:
69981 *
69982 * var types = Ext.data.Types; // allow shorthand type access
69983 * Ext.define('Unit', {
69984 * extend: 'Ext.data.Model',
69985 * config: {
69986 * fields: [
69987 * { name: 'unitName', mapping: 'UnitName' },
69988 * { name: 'curSpeed', mapping: 'CurSpeed', type: types.INT },
69989 * { name: 'latitude', mapping: 'lat', type: types.FLOAT },
69990 * { name: 'position', type: types.VELATLONG }
69991 * ]
69992 * }
69993 * });
69994 *
69995 * @singleton
69996 */
69997 Ext.define('Ext.data.Types', {
69998 singleton: true,
69999
70000
70001 /**
70002 * @property {RegExp} stripRe
70003 * A regular expression for stripping non-numeric characters from a numeric value.
70004 * This should be overridden for localization.
70005 */
70006 stripRe: /[\$,%]/g,
70007 dashesRe: /-/g,
70008 iso8601TestRe: /\d\dT\d\d/,
70009 iso8601SplitRe: /[- :T\.Z\+]/
70010
70011 }, function() {
70012 var Types = this,
70013 sortTypes = Ext.data.SortTypes;
70014
70015 Ext.apply(Types, {
70016 /**
70017 * @property {Object} AUTO
70018 * This data type means that no conversion is applied to the raw data before it is placed into a Record.
70019 */
70020 AUTO: {
70021 convert: function(value) {
70022 return value;
70023 },
70024 sortType: sortTypes.none,
70025 type: 'auto'
70026 },
70027
70028 /**
70029 * @property {Object} STRING
70030 * This data type means that the raw data is converted into a String before it is placed into a Record.
70031 */
70032 STRING: {
70033 convert: function(value) {
70034 // 'this' is the actual field that calls this convert method
70035 return (value === undefined || value === null)
70036 ? (this.getAllowNull() ? null : '')
70037 : String(value);
70038 },
70039 sortType: sortTypes.asUCString,
70040 type: 'string'
70041 },
70042
70043 /**
70044 * @property {Object} INT
70045 * This data type means that the raw data is converted into an integer before it is placed into a Record.
70046 * A standard javascript parseInt(foo, 10) is enforced.
70047 *
70048 * The synonym `INTEGER` is equivalent.
70049 */
70050 INT: {
70051 convert: function(value) {
70052 return (value !== undefined && value !== null && value !== '')
70053 ? ((typeof value === 'number')
70054 ? parseInt(value, 10)
70055 : parseInt(String(value).replace(Types.stripRe, ''), 10)
70056 )
70057 : (this.getAllowNull() ? null : 0);
70058 },
70059 sortType: sortTypes.none,
70060 type: 'int'
70061 },
70062
70063 /**
70064 * @property {Object} FLOAT
70065 * This data type means that the raw data is converted into a number before it is placed into a Record.
70066 * A standard javascript parseFloat(foo, 10) is enforced.
70067 *
70068 * The synonym `NUMBER` is equivalent.
70069 */
70070 FLOAT: {
70071 convert: function(value) {
70072 return (value !== undefined && value !== null && value !== '')
70073 ? ((typeof value === 'number')
70074 ? value
70075 : parseFloat(String(value).replace(Types.stripRe, ''), 10)
70076 )
70077 : (this.getAllowNull() ? null : 0);
70078 },
70079 sortType: sortTypes.none,
70080 type: 'float'
70081 },
70082
70083 /**
70084 * @property {Object} BOOL
70085 * This data type means that the raw data is converted into a Boolean before it is placed into
70086 * a Record. The string "true", "1" and the number 1 are converted to Boolean `true`. The String "0" will be converted to Boolean 'false'.
70087 *
70088 * The synonym `BOOLEAN` is equivalent.
70089 */
70090 BOOL: {
70091 convert: function(value) {
70092 if ((value === undefined || value === null || value === '') && this.getAllowNull()) {
70093 return null;
70094 }
70095 return value !== 'false' && value !== '0' && !!value;
70096 },
70097 sortType: sortTypes.none,
70098 type: 'bool'
70099 },
70100
70101 /**
70102 * @property {Object} DATE
70103 * This data type means that the raw data is converted into a Date before it is placed into a Record.
70104 * The date format is specified in the constructor of the {@link Ext.data.Field} to which this type is
70105 * being applied.
70106 */
70107 DATE: {
70108 convert: function(value) {
70109 var dateFormat = this.getDateFormat(),
70110 parsed;
70111
70112 if (!value) {
70113 return null;
70114 }
70115 if (Ext.isDate(value)) {
70116 return value;
70117 }
70118 if (dateFormat) {
70119 if (dateFormat == 'timestamp') {
70120 return new Date(value*1000);
70121 }
70122 if (dateFormat == 'time') {
70123 return new Date(parseInt(value, 10));
70124 }
70125 return Ext.Date.parse(value, dateFormat);
70126 }
70127
70128 parsed = new Date(Date.parse(value));
70129 if (isNaN(parsed)) {
70130 // Dates with ISO 8601 format are not well supported by mobile devices, this can work around the issue.
70131 if (Types.iso8601TestRe.test(value)) {
70132 parsed = value.split(Types.iso8601SplitRe);
70133 parsed = new Date(parsed[0], parsed[1]-1, parsed[2], parsed[3], parsed[4], parsed[5]);
70134 }
70135 if (isNaN(parsed)) {
70136 // Dates with the format "2012-01-20" fail, but "2012/01/20" work in some browsers. We'll try and
70137 // get around that.
70138 parsed = new Date(Date.parse(value.replace(Types.dashesRe, "/")));
70139 //<debug>
70140 if (isNaN(parsed)) {
70141 Ext.Logger.warn("Cannot parse the passed value (" + value + ") into a valid date");
70142 }
70143 //</debug>
70144 }
70145 }
70146
70147 return isNaN(parsed) ? null : parsed;
70148 },
70149 sortType: sortTypes.asDate,
70150 type: 'date'
70151 }
70152 });
70153
70154 Ext.apply(Types, {
70155 /**
70156 * @property {Object} BOOLEAN
70157 * This data type means that the raw data is converted into a Boolean before it is placed into
70158 * a Record. The string "true" and the number 1 are converted to Boolean `true`.
70159 *
70160 * The synonym `BOOL` is equivalent.
70161 */
70162 BOOLEAN: this.BOOL,
70163
70164 /**
70165 * @property {Object} INTEGER
70166 * This data type means that the raw data is converted into an integer before it is placed into a Record.
70167 *
70168 *The synonym `INT` is equivalent.
70169 */
70170 INTEGER: this.INT,
70171
70172 /**
70173 * @property {Object} NUMBER
70174 * This data type means that the raw data is converted into a number before it is placed into a Record.
70175 *
70176 * The synonym `FLOAT` is equivalent.
70177 */
70178 NUMBER: this.FLOAT
70179 });
70180 });
70181
70182 /**
70183 * @author Ed Spencer
70184 *
70185 * Fields are used to define what a Model is. They aren't instantiated directly - instead, when we create a class that
70186 * extends {@link Ext.data.Model}, it will automatically create a Field instance for each field configured in a {@link
70187 * Ext.data.Model Model}. For example, we might set up a model like this:
70188 *
70189 * Ext.define('User', {
70190 * extend: 'Ext.data.Model',
70191 * config: {
70192 * fields: [
70193 * 'name', 'email',
70194 * {name: 'age', type: 'int'},
70195 * {name: 'gender', type: 'string', defaultValue: 'Unknown'}
70196 * ]
70197 * }
70198 * });
70199 *
70200 * Four fields will have been created for the User Model - name, email, age, and gender. Note that we specified a couple
70201 * of different formats here; if we only pass in the string name of the field (as with name and email), the field is set
70202 * up with the 'auto' type. It's as if we'd done this instead:
70203 *
70204 * Ext.define('User', {
70205 * extend: 'Ext.data.Model',
70206 * config: {
70207 * fields: [
70208 * {name: 'name', type: 'auto'},
70209 * {name: 'email', type: 'auto'},
70210 * {name: 'age', type: 'int'},
70211 * {name: 'gender', type: 'string', defaultValue: 'Unknown'}
70212 * ]
70213 * }
70214 * });
70215 *
70216 * # Types and conversion
70217 *
70218 * The {@link #type} is important - it's used to automatically convert data passed to the field into the correct format.
70219 * In our example above, the name and email fields used the 'auto' type and will just accept anything that is passed
70220 * into them. The 'age' field had an 'int' type however, so if we passed 25.4 this would be rounded to 25.
70221 *
70222 * Sometimes a simple type isn't enough, or we want to perform some processing when we load a Field's data. We can do
70223 * this using a {@link #convert} function. Here, we're going to create a new field based on another:
70224 *
70225 * Ext.define('User', {
70226 * extend: 'Ext.data.Model',
70227 * config: {
70228 * fields: [
70229 * 'name', 'email',
70230 * {name: 'age', type: 'int'},
70231 * {name: 'gender', type: 'string', defaultValue: 'Unknown'},
70232 *
70233 * {
70234 * name: 'firstName',
70235 * convert: function(value, record) {
70236 * var fullName = record.get('name'),
70237 * splits = fullName.split(" "),
70238 * firstName = splits[0];
70239 *
70240 * return firstName;
70241 * }
70242 * }
70243 * ]
70244 * }
70245 * });
70246 *
70247 * Now when we create a new User, the firstName is populated automatically based on the name:
70248 *
70249 * var ed = Ext.create('User', {name: 'Ed Spencer'});
70250 *
70251 * console.log(ed.get('firstName')); //logs 'Ed', based on our convert function
70252 *
70253 * In fact, if we log out all of the data inside ed, we'll see this:
70254 *
70255 * console.log(ed.data);
70256 *
70257 * //outputs this:
70258 * {
70259 * age: 0,
70260 * email: "",
70261 * firstName: "Ed",
70262 * gender: "Unknown",
70263 * name: "Ed Spencer"
70264 * }
70265 *
70266 * The age field has been given a default of zero because we made it an int type. As an auto field, email has defaulted
70267 * to an empty string. When we registered the User model we set gender's {@link #defaultValue} to 'Unknown' so we see
70268 * that now. Let's correct that and satisfy ourselves that the types work as we expect:
70269 *
70270 * ed.set('gender', 'Male');
70271 * ed.get('gender'); //returns 'Male'
70272 *
70273 * ed.set('age', 25.4);
70274 * ed.get('age'); //returns 25 - we wanted an int, not a float, so no decimal places allowed
70275 *
70276 * ###Further Reading
70277 * [Sencha Touch Data Overview](../../../core_concepts/data/data_package_overview.html)
70278 * [Sencha Touch Store Guide](../../../core_concepts/data/stores.html)
70279 * [Sencha Touch Models Guide](../../../core_concepts/data/models.html)
70280 * [Sencha Touch Proxy Guide](../../../core_concepts/data/proxies.html)
70281 */
70282 Ext.define('Ext.data.Field', {
70283
70284 alias: 'data.field',
70285
70286 isField: true,
70287
70288 config: {
70289 /**
70290 * @cfg {String} name
70291 *
70292 * The name by which the field is referenced within the Model. This is referenced by, for example, the `dataIndex`
70293 * property in column definition objects passed to Ext.grid.property.HeaderContainer.
70294 *
70295 * Note: In the simplest case, if no properties other than `name` are required, a field definition may consist of
70296 * just a String for the field name.
70297 */
70298 name: null,
70299
70300 /**
70301 * @cfg {String/Object} type
70302 *
70303 * The data type for automatic conversion from received data to the *stored* value if
70304 * `{@link Ext.data.Field#convert convert}` has not been specified. This may be specified as a string value.
70305 * Possible values are
70306 *
70307 * - auto (Default, implies no conversion)
70308 * - string
70309 * - int
70310 * - float
70311 * - boolean
70312 * - date
70313 *
70314 * This may also be specified by referencing a member of the {@link Ext.data.Types} class.
70315 *
70316 * Developers may create their own application-specific data types by defining new members of the {@link
70317 * Ext.data.Types} class.
70318 */
70319 type: 'auto',
70320
70321 /**
70322 * @cfg {Function} convert
70323 *
70324 * A function which converts the value provided by the Reader into an object that will be stored in the Model.
70325 * It is passed the following parameters:
70326 *
70327 * - **v** : Mixed
70328 *
70329 * The data value as read by the Reader, if undefined will use the configured `{@link Ext.data.Field#defaultValue
70330 * defaultValue}`.
70331 *
70332 * - **rec** : Ext.data.Model
70333 *
70334 * The data object containing the Model as read so far by the Reader. Note that the Model may not be fully populated
70335 * at this point as the fields are read in the order that they are defined in your
70336 * {@link Ext.data.Model#cfg-fields fields} array.
70337 *
70338 * Example of convert functions:
70339 *
70340 * function fullName(v, record) {
70341 * return record.name.last + ', ' + record.name.first;
70342 * }
70343 *
70344 * function location(v, record) {
70345 * return !record.city ? '' : (record.city + ', ' + record.state);
70346 * }
70347 *
70348 * Ext.define('Dude', {
70349 * extend: 'Ext.data.Model',
70350 * fields: [
70351 * {name: 'fullname', convert: fullName},
70352 * {name: 'firstname', mapping: 'name.first'},
70353 * {name: 'lastname', mapping: 'name.last'},
70354 * {name: 'city', defaultValue: 'homeless'},
70355 * 'state',
70356 * {name: 'location', convert: location}
70357 * ]
70358 * });
70359 *
70360 * // create the data store
70361 * var store = Ext.create('Ext.data.Store', {
70362 * reader: {
70363 * type: 'json',
70364 * model: 'Dude',
70365 * idProperty: 'key',
70366 * rootProperty: 'daRoot',
70367 * totalProperty: 'total'
70368 * }
70369 * });
70370 *
70371 * var myData = [
70372 * { key: 1,
70373 * name: { first: 'Fat', last: 'Albert' }
70374 * // notice no city, state provided in data2 object
70375 * },
70376 * { key: 2,
70377 * name: { first: 'Barney', last: 'Rubble' },
70378 * city: 'Bedrock', state: 'Stoneridge'
70379 * },
70380 * { key: 3,
70381 * name: { first: 'Cliff', last: 'Claven' },
70382 * city: 'Boston', state: 'MA'
70383 * }
70384 * ];
70385 */
70386 convert: undefined,
70387
70388 /**
70389 * @cfg {String} dateFormat
70390 *
70391 * Used when converting received data into a Date when the {@link #type} is specified as `"date"`.
70392 *
70393 * A format string for the {@link Ext.Date#parse Ext.Date.parse} function, or "timestamp" if the value provided by
70394 * the Reader is a UNIX timestamp, or "time" if the value provided by the Reader is a JavaScript millisecond
70395 * timestamp. See {@link Ext.Date}.
70396 */
70397 dateFormat: null,
70398
70399 /**
70400 * @cfg {Boolean} allowNull
70401 *
70402 * Use when converting received data into a boolean, string or number type (either int or float). If the value cannot be
70403 * parsed, `null` will be used if `allowNull` is `true`, otherwise the value will be 0.
70404 */
70405 allowNull: true,
70406
70407 /**
70408 * @cfg {Object} [defaultValue='']
70409 *
70410 * The default value used **when a Model is being created by a {@link Ext.data.reader.Reader Reader}**
70411 * when the item referenced by the `{@link Ext.data.Field#mapping mapping}` does not exist in the data object
70412 * (i.e. `undefined`).
70413 */
70414 defaultValue: undefined,
70415
70416 /**
70417 * @cfg {String/Number} mapping
70418 *
70419 * (Optional) A path expression for use by the {@link Ext.data.reader.Reader} implementation that is creating the
70420 * {@link Ext.data.Model Model} to extract the Field value from the data object. If the path expression is the same
70421 * as the field name, the mapping may be omitted.
70422 *
70423 * The form of the mapping expression depends on the Reader being used.
70424 *
70425 * - {@link Ext.data.reader.Json}
70426 *
70427 * The mapping is a string containing the JavaScript expression to reference the data from an element of the data2
70428 * item's {@link Ext.data.reader.Json#rootProperty rootProperty} Array. Defaults to the field name.
70429 *
70430 * - {@link Ext.data.reader.Xml}
70431 *
70432 * The mapping is an {@link Ext.DomQuery} path to the data item relative to the DOM element that represents the
70433 * {@link Ext.data.reader.Xml#record record}. Defaults to the field name.
70434 *
70435 * - {@link Ext.data.reader.Array}
70436 *
70437 * The mapping is a number indicating the Array index of the field's value. Defaults to the field specification's
70438 * Array position.
70439 *
70440 * If a more complex value extraction strategy is required, then configure the Field with a {@link #convert}
70441 * function. This is passed the whole row object, and may interrogate it in whatever way is necessary in order to
70442 * return the desired data.
70443 */
70444 mapping: null,
70445
70446 /**
70447 * @cfg {Function} sortType
70448 *
70449 * A function which converts a Field's value to a comparable value in order to ensure correct sort ordering.
70450 * Predefined functions are provided in {@link Ext.data.SortTypes}. A custom sort example:
70451 *
70452 * // current sort after sort we want
70453 * // +-+------+ +-+------+
70454 * // |1|First | |1|First |
70455 * // |2|Last | |3|Second|
70456 * // |3|Second| |2|Last |
70457 * // +-+------+ +-+------+
70458 *
70459 * sortType: function(value) {
70460 * switch (value.toLowerCase()) // native toLowerCase():
70461 * {
70462 * case 'first': return 1;
70463 * case 'second': return 2;
70464 * default: return 3;
70465 * }
70466 * }
70467 */
70468 sortType : undefined,
70469
70470 /**
70471 * @cfg {String} sortDir
70472 *
70473 * Initial direction to sort (`"ASC"` or `"DESC"`).
70474 */
70475 sortDir : "ASC",
70476
70477 /**
70478 * @cfg {Boolean} allowBlank
70479 * @private
70480 *
70481 * Used for validating a {@link Ext.data.Model model}. An empty value here will cause
70482 * {@link Ext.data.Model}.{@link Ext.data.Model#isValid isValid} to evaluate to `false`.
70483 */
70484 allowBlank : true,
70485
70486 /**
70487 * @cfg {Boolean} persist
70488 *
70489 * `false` to exclude this field from being synchronized with the server or localStorage.
70490 * This option is useful when model fields are used to keep state on the client but do
70491 * not need to be persisted to the server.
70492 */
70493 persist: true,
70494
70495 // Used in LocalStorage stuff
70496 encode: null,
70497 decode: null,
70498
70499 bubbleEvents: 'action'
70500 },
70501
70502 constructor : function(config) {
70503 // This adds support for just passing a string used as the field name
70504 if (Ext.isString(config)) {
70505 config = {name: config};
70506 }
70507
70508 this.initConfig(config);
70509 },
70510
70511 applyType: function(type) {
70512 var types = Ext.data.Types,
70513 autoType = types.AUTO;
70514
70515 if (type) {
70516 if (Ext.isString(type)) {
70517 return types[type.toUpperCase()] || autoType;
70518 } else {
70519 // At this point we expect an actual type
70520 return type;
70521 }
70522 }
70523
70524 return autoType;
70525 },
70526
70527 updateType: function(newType, oldType) {
70528 var convert = this.getConvert();
70529 if (oldType && convert === oldType.convert) {
70530 this.setConvert(newType.convert);
70531 }
70532 },
70533
70534 applySortType: function(sortType) {
70535 var sortTypes = Ext.data.SortTypes,
70536 type = this.getType(),
70537 defaultSortType = type.sortType;
70538
70539 if (sortType) {
70540 if (Ext.isString(sortType)) {
70541 return sortTypes[sortType] || defaultSortType;
70542 } else {
70543 // At this point we expect a function
70544 return sortType;
70545 }
70546 }
70547
70548 return defaultSortType;
70549 },
70550
70551 applyConvert: function(convert) {
70552 var defaultConvert = this.getType().convert;
70553 if (convert && convert !== defaultConvert) {
70554 this._hasCustomConvert = true;
70555 return convert;
70556 } else {
70557 this._hasCustomConvert = false;
70558 return defaultConvert;
70559 }
70560 },
70561
70562 hasCustomConvert: function() {
70563 return this._hasCustomConvert;
70564 }
70565
70566 });
70567
70568 /**
70569 * @author Tommy Maintz
70570 *
70571 * This class is the simple default id generator for Model instances.
70572 *
70573 * An example of a configured simple generator would be:
70574 *
70575 * Ext.define('MyApp.data.MyModel', {
70576 * extend: 'Ext.data.Model',
70577 * config: {
70578 * identifier: {
70579 * type: 'simple',
70580 * prefix: 'ID_'
70581 * }
70582 * }
70583 * });
70584 * // assign id's of ID_1, ID_2, ID_3, etc.
70585 *
70586 */
70587 Ext.define('Ext.data.identifier.Simple', {
70588 alias: 'data.identifier.simple',
70589
70590 statics: {
70591 AUTO_ID: 1
70592 },
70593
70594 config: {
70595 prefix: 'ext-record-'
70596 },
70597
70598 constructor: function(config) {
70599 this.initConfig(config);
70600 },
70601
70602 generate: function(record) {
70603 return this._prefix + this.self.AUTO_ID++;
70604 }
70605 });
70606
70607 /**
70608 * @author Ed Spencer
70609 *
70610 * The ModelManager keeps track of all {@link Ext.data.Model} types defined in your application.
70611 *
70612 * ## Creating Model Instances
70613 *
70614 * Model instances can be created by using the {@link Ext.ClassManager#create Ext.create} method. Ext.create replaces
70615 * the deprecated {@link #create Ext.ModelManager.create} method. It is also possible to create a model instance
70616 * this by using the Model type directly. The following 3 snippets are equivalent:
70617 *
70618 * Ext.define('User', {
70619 * extend: 'Ext.data.Model',
70620 * config: {
70621 * fields: ['first', 'last']
70622 * }
70623 * });
70624 *
70625 * // method 1, create using Ext.create (recommended)
70626 * Ext.create('User', {
70627 * first: 'Ed',
70628 * last: 'Spencer'
70629 * });
70630 *
70631 * // method 2, create on the type directly
70632 * new User({
70633 * first: 'Ed',
70634 * last: 'Spencer'
70635 * });
70636 *
70637 * // method 3, create through the manager (deprecated)
70638 * Ext.ModelManager.create({
70639 * first: 'Ed',
70640 * last: 'Spencer'
70641 * }, 'User');
70642 *
70643 * ## Accessing Model Types
70644 *
70645 * A reference to a Model type can be obtained by using the {@link #getModel} function. Since models types
70646 * are normal classes, you can access the type directly. The following snippets are equivalent:
70647 *
70648 * Ext.define('User', {
70649 * extend: 'Ext.data.Model',
70650 * config: {
70651 * fields: ['first', 'last']
70652 * }
70653 * });
70654 *
70655 * // method 1, access model type through the manager
70656 * var UserType = Ext.ModelManager.getModel('User');
70657 *
70658 * // method 2, reference the type directly
70659 * var UserType = User;
70660 */
70661 Ext.define('Ext.data.ModelManager', {
70662 extend: Ext.AbstractManager ,
70663 alternateClassName: ['Ext.ModelMgr', 'Ext.ModelManager'],
70664
70665 singleton: true,
70666
70667 /**
70668 * @property defaultProxyType
70669 * The string type of the default Model Proxy.
70670 * @removed 2.0.0
70671 */
70672
70673 /**
70674 * @property associationStack
70675 * Private stack of associations that must be created once their associated model has been defined.
70676 * @removed 2.0.0
70677 */
70678
70679 modelNamespace: null,
70680
70681 /**
70682 * Registers a model definition. All model plugins marked with `isDefault: true` are bootstrapped
70683 * immediately, as are any addition plugins defined in the model config.
70684 * @param {String} name
70685 * @param {Object} config
70686 * @return {Object}
70687 */
70688 registerType: function(name, config) {
70689 var proto = config.prototype,
70690 model;
70691
70692 if (proto && proto.isModel) {
70693 // registering an already defined model
70694 model = config;
70695 } else {
70696 config = {
70697 extend: config.extend || 'Ext.data.Model',
70698 config: config
70699 };
70700 model = Ext.define(name, config);
70701 }
70702 this.types[name] = model;
70703 return model;
70704 },
70705
70706 onModelDefined: Ext.emptyFn,
70707
70708 // /**
70709 // * @private
70710 // * Private callback called whenever a model has just been defined. This sets up any associations
70711 // * that were waiting for the given model to be defined.
70712 // * @param {Function} model The model that was just created.
70713 // */
70714 // onModelDefined: function(model) {
70715 // var stack = this.associationStack,
70716 // length = stack.length,
70717 // create = [],
70718 // association, i, created;
70719 //
70720 // for (i = 0; i < length; i++) {
70721 // association = stack[i];
70722 //
70723 // if (association.associatedModel == model.modelName) {
70724 // create.push(association);
70725 // }
70726 // }
70727 //
70728 // for (i = 0, length = create.length; i < length; i++) {
70729 // created = create[i];
70730 // this.types[created.ownerModel].prototype.associations.add(Ext.data.association.Association.create(created));
70731 // Ext.Array.remove(stack, created);
70732 // }
70733 // },
70734 //
70735 // /**
70736 // * Registers an association where one of the models defined doesn't exist yet.
70737 // * The ModelManager will check when new models are registered if it can link them
70738 // * together.
70739 // * @private
70740 // * @param {Ext.data.association.Association} association The association.
70741 // */
70742 // registerDeferredAssociation: function(association){
70743 // this.associationStack.push(association);
70744 // },
70745
70746 /**
70747 * Returns the {@link Ext.data.Model} for a given model name.
70748 * @param {String/Object} id The `id` of the model or the model instance.
70749 * @return {Ext.data.Model} A model class.
70750 */
70751 getModel: function(id) {
70752 var model = id;
70753 if (typeof model == 'string') {
70754 model = this.types[model];
70755 if (!model && this.modelNamespace) {
70756 model = this.types[this.modelNamespace + '.' + model];
70757 }
70758 }
70759 return model;
70760 },
70761
70762 /**
70763 * Creates a new instance of a Model using the given data.
70764 *
70765 * __Note:__ This method is deprecated. Use {@link Ext.ClassManager#create Ext.create} instead. For example:
70766 *
70767 * Ext.create('User', {
70768 * first: 'Ed',
70769 * last: 'Spencer'
70770 * });
70771 *
70772 * @param {Object} data Data to initialize the Model's fields with.
70773 * @param {String} name The name of the model to create.
70774 * @param {Number} id (optional) Unique id of the Model instance (see {@link Ext.data.Model}).
70775 * @return {Object}
70776 */
70777 create: function(config, name, id) {
70778 var con = typeof name == 'function' ? name : this.types[name || config.name];
70779 return new con(config, id);
70780 }
70781 }, function() {
70782
70783 /**
70784 * Old way for creating Model classes. Instead use:
70785 *
70786 * Ext.define("MyModel", {
70787 * extend: "Ext.data.Model",
70788 * fields: []
70789 * });
70790 *
70791 * @param {String} name Name of the Model class.
70792 * @param {Object} config A configuration object for the Model you wish to create.
70793 * @return {Ext.data.Model} The newly registered Model.
70794 * @member Ext
70795 * @deprecated 2.0.0 Please use {@link Ext#define} instead.
70796 */
70797 Ext.regModel = function() {
70798 //<debug>
70799 Ext.Logger.deprecate('Ext.regModel has been deprecated. Models can now be created by ' +
70800 'extending Ext.data.Model: Ext.define("MyModel", {extend: "Ext.data.Model", fields: []});.');
70801 //</debug>
70802 return this.ModelManager.registerType.apply(this.ModelManager, arguments);
70803 };
70804 });
70805
70806 /**
70807 * @author Ed Spencer
70808 *
70809 * Simple class that represents a Request that will be made by any {@link Ext.data.proxy.Server} subclass.
70810 * All this class does is standardize the representation of a Request as used by any ServerProxy subclass,
70811 * it does not contain any actual logic or perform the request itself.
70812 */
70813 Ext.define('Ext.data.Request', {
70814 config: {
70815 /**
70816 * @cfg {String} action
70817 * The name of the action this Request represents. Usually one of 'create', 'read', 'update' or 'destroy'.
70818 */
70819 action: null,
70820
70821 /**
70822 * @cfg {Object} params
70823 * HTTP request params. The Proxy and its Writer have access to and can modify this object.
70824 */
70825 params: null,
70826
70827 /**
70828 * @cfg {String} method
70829 * The HTTP method to use on this Request. Should be one of 'GET', 'POST', 'PUT' or 'DELETE'.
70830 */
70831 method: 'GET',
70832
70833 /**
70834 * @cfg {String} url
70835 * The url to access on this Request.
70836 */
70837 url: null,
70838
70839 /**
70840 * @cfg {Ext.data.Operation} operation
70841 * The operation this request belongs to.
70842 */
70843 operation: null,
70844
70845 /**
70846 * @cfg {Ext.data.proxy.Proxy} proxy
70847 * The proxy this request belongs to.
70848 */
70849 proxy: null,
70850
70851 /**
70852 * @cfg {Boolean} disableCaching
70853 * Whether or not to disable caching for this request.
70854 */
70855 disableCaching: false,
70856
70857 /**
70858 * @cfg {Object} headers
70859 * Some requests (like XMLHttpRequests) want to send additional server headers.
70860 * This configuration can be set for those types of requests.
70861 */
70862 headers: {},
70863
70864 /**
70865 * @cfg {String} callbackKey
70866 * Some requests (like JsonP) want to send an additional key that contains
70867 * the name of the callback function.
70868 */
70869 callbackKey: null,
70870
70871 /**
70872 * @cfg {Ext.data.JsonP} jsonp
70873 * JsonP requests return a handle that might be useful in the callback function.
70874 */
70875 jsonP: null,
70876
70877 /**
70878 * @cfg {Object} jsonData
70879 * This is used by some write actions to attach data to the request without encoding it
70880 * as a parameter.
70881 */
70882 jsonData: null,
70883
70884 /**
70885 * @cfg {Object} xmlData
70886 * This is used by some write actions to attach data to the request without encoding it
70887 * as a parameter, but instead sending it as XML.
70888 */
70889 xmlData: null,
70890
70891 /**
70892 * @cfg {Boolean} withCredentials
70893 * This field is necessary when using cross-origin resource sharing.
70894 */
70895 withCredentials: null,
70896
70897 /**
70898 * @cfg {String} username
70899 * Most oData feeds require basic HTTP authentication. This configuration allows
70900 * you to specify the username.
70901 * @accessor
70902 */
70903 username: null,
70904
70905 /**
70906 * @cfg {String} password
70907 * Most oData feeds require basic HTTP authentication. This configuration allows
70908 * you to specify the password.
70909 * @accessor
70910 */
70911 password: null,
70912
70913 callback: null,
70914 scope: null,
70915 timeout: 30000,
70916 records: null,
70917
70918 // The following two configurations are only used by Ext.data.proxy.Direct and are just
70919 // for being able to retrieve them after the request comes back from the server.
70920 directFn: null,
70921 args: null,
70922 useDefaultXhrHeader: null
70923 },
70924
70925 /**
70926 * Creates the Request object.
70927 * @param {Object} [config] Config object.
70928 */
70929 constructor: function(config) {
70930 this.initConfig(config);
70931 }
70932 });
70933
70934 /**
70935 * @author Ed Spencer
70936 *
70937 * ServerProxy is a superclass of {@link Ext.data.proxy.JsonP JsonPProxy} and {@link Ext.data.proxy.Ajax AjaxProxy}, and
70938 * would not usually be used directly.
70939 * @private
70940 */
70941 Ext.define('Ext.data.proxy.Server', {
70942 extend: Ext.data.proxy.Proxy ,
70943 alias : 'proxy.server',
70944 alternateClassName: 'Ext.data.ServerProxy',
70945
70946
70947 config: {
70948 /**
70949 * @cfg {String} url
70950 * The URL from which to request the data object.
70951 */
70952 url: null,
70953
70954 /**
70955 * @cfg {String} pageParam
70956 * The name of the `page` parameter to send in a request. Set this to `false` if you don't
70957 * want to send a page parameter.
70958 */
70959 pageParam: 'page',
70960
70961 /**
70962 * @cfg {String} startParam
70963 * The name of the `start` parameter to send in a request. Set this to `false` if you don't
70964 * want to send a start parameter.
70965 */
70966 startParam: 'start',
70967
70968 /**
70969 * @cfg {String} limitParam
70970 * The name of the `limit` parameter to send in a request. Set this to `false` if you don't
70971 * want to send a limit parameter.
70972 */
70973 limitParam: 'limit',
70974
70975 /**
70976 * @cfg {String} groupParam
70977 * The name of the `group` parameter to send in a request. Set this to `false` if you don't
70978 * want to send a group parameter.
70979 */
70980 groupParam: 'group',
70981
70982 /**
70983 * @cfg {String} sortParam
70984 * The name of the `sort` parameter to send in a request. Set this to `undefined` if you don't
70985 * want to send a sort parameter.
70986 */
70987 sortParam: 'sort',
70988
70989 /**
70990 * @cfg {String} filterParam
70991 * The name of the 'filter' parameter to send in a request. Set this to `undefined` if you don't
70992 * want to send a filter parameter.
70993 */
70994 filterParam: 'filter',
70995
70996 /**
70997 * @cfg {String} directionParam
70998 * The name of the direction parameter to send in a request.
70999 *
71000 * __Note:__ This is only used when `simpleSortMode` is set to `true`.
71001 */
71002 directionParam: 'dir',
71003
71004 /**
71005 * @cfg {Boolean} enablePagingParams This can be set to `false` if you want to prevent the paging params to be
71006 * sent along with the requests made by this proxy.
71007 */
71008 enablePagingParams: true,
71009
71010 /**
71011 * @cfg {Boolean} simpleSortMode
71012 * Enabling `simpleSortMode` in conjunction with `remoteSort` will only send one sort property and a direction when a
71013 * remote sort is requested. The `directionParam` and `sortParam` will be sent with the property name and either 'ASC'
71014 * or 'DESC'.
71015 */
71016 simpleSortMode: false,
71017
71018 /**
71019 * @cfg {Boolean} noCache
71020 * Disable caching by adding a unique parameter name to the request. Set to `false` to allow caching.
71021 */
71022 noCache : true,
71023
71024 /**
71025 * @cfg {String} cacheString
71026 * The name of the cache param added to the url when using `noCache`.
71027 */
71028 cacheString: "_dc",
71029
71030 /**
71031 * @cfg {Number} timeout
71032 * The number of milliseconds to wait for a response.
71033 */
71034 timeout : 30000,
71035
71036 /**
71037 * @cfg {Object} api
71038 * Specific urls to call on CRUD action methods "create", "read", "update" and "destroy". Defaults to:
71039 *
71040 * api: {
71041 * create : undefined,
71042 * read : undefined,
71043 * update : undefined,
71044 * destroy : undefined
71045 * }
71046 *
71047 * The url is built based upon the action being executed [create|read|update|destroy] using the commensurate
71048 * {@link #api} property, or if undefined default to the configured
71049 * {@link Ext.data.Store}.{@link Ext.data.proxy.Server#url url}.
71050 *
71051 * For example:
71052 *
71053 * api: {
71054 * create : '/controller/new',
71055 * read : '/controller/load',
71056 * update : '/controller/update',
71057 * destroy : '/controller/destroy_action'
71058 * }
71059 *
71060 * If the specific URL for a given CRUD action is undefined, the CRUD action request will be directed to the
71061 * configured {@link Ext.data.proxy.Server#url url}.
71062 */
71063 api: {
71064 create : undefined,
71065 read : undefined,
71066 update : undefined,
71067 destroy : undefined
71068 },
71069
71070 /**
71071 * @cfg {Object} extraParams
71072 * Extra parameters that will be included on every request. Individual requests with params of the same name
71073 * will override these params when they are in conflict.
71074 */
71075 extraParams: {}
71076 },
71077
71078 constructor: function(config) {
71079 config = config || {};
71080 if (config.nocache !== undefined) {
71081 config.noCache = config.nocache;
71082 // <debug>
71083 Ext.Logger.warn('nocache configuration on Ext.data.proxy.Server has been deprecated. Please use noCache.');
71084 // </debug>
71085 }
71086 this.callParent([config]);
71087 },
71088
71089 //in a ServerProxy all four CRUD operations are executed in the same manner, so we delegate to doRequest in each case
71090 create: function() {
71091 return this.doRequest.apply(this, arguments);
71092 },
71093
71094 read: function() {
71095 return this.doRequest.apply(this, arguments);
71096 },
71097
71098 update: function() {
71099 return this.doRequest.apply(this, arguments);
71100 },
71101
71102 destroy: function() {
71103 return this.doRequest.apply(this, arguments);
71104 },
71105
71106 /**
71107 * Sets a value in the underlying {@link #extraParams}.
71108 * @param {String} name The key for the new value
71109 * @param {Object} value The value
71110 */
71111 setExtraParam: function(name, value) {
71112 this.getExtraParams()[name] = value;
71113 },
71114
71115 /**
71116 * Creates and returns an Ext.data.Request object based on the options passed by the {@link Ext.data.Store Store}
71117 * that this Proxy is attached to.
71118 * @param {Ext.data.Operation} operation The {@link Ext.data.Operation Operation} object to execute
71119 * @return {Ext.data.Request} The request object
71120 */
71121 buildRequest: function(operation) {
71122 var me = this,
71123 params = Ext.applyIf(operation.getParams() || {}, me.getExtraParams() || {}),
71124 request;
71125
71126 //copy any sorters, filters etc into the params so they can be sent over the wire
71127 params = Ext.applyIf(params, me.getParams(operation));
71128
71129 request = Ext.create('Ext.data.Request', {
71130 params : params,
71131 action : operation.getAction(),
71132 records : operation.getRecords(),
71133 url : operation.getUrl(),
71134 operation: operation,
71135 proxy : me
71136 });
71137
71138 request.setUrl(me.buildUrl(request));
71139 operation.setRequest(request);
71140
71141 return request;
71142 },
71143
71144 /**
71145 * This method handles the processing of the response and is usually overridden by subclasses to
71146 * do additional processing.
71147 * @param {Boolean} success Whether or not this request was successful
71148 * @param {Ext.data.Operation} operation The operation we made this request for
71149 * @param {Ext.data.Request} request The request that was made
71150 * @param {Object} response The response that we got
71151 * @param {Function} callback The callback to be fired onces the response is processed
71152 * @param {Object} scope The scope in which we call the callback
71153 * @protected
71154 */
71155 processResponse: function(success, operation, request, response, callback, scope) {
71156 var me = this,
71157 action = operation.getAction(),
71158 reader, resultSet;
71159
71160 if (success === true) {
71161 reader = me.getReader();
71162
71163 try {
71164 resultSet = reader.process(me.getResponseResult(response));
71165 } catch(e) {
71166 operation.setException(e.message);
71167
71168 me.fireEvent('exception', me, response, operation);
71169 return;
71170 }
71171
71172 // This could happen if the model was configured using metaData
71173 if (!operation.getModel()) {
71174 operation.setModel(this.getModel());
71175 }
71176
71177 if (operation.process(action, resultSet, request, response) === false) {
71178 me.setException(operation, response);
71179 me.fireEvent('exception', me, response, operation);
71180 }
71181 } else {
71182 me.setException(operation, response);
71183 /**
71184 * @event exception
71185 * Fires when the server returns an exception
71186 * @param {Ext.data.proxy.Proxy} this
71187 * @param {Object} response The response from the AJAX request
71188 * @param {Ext.data.Operation} operation The operation that triggered request
71189 */
71190 me.fireEvent('exception', this, response, operation);
71191 }
71192
71193 //this callback is the one that was passed to the 'read' or 'write' function above
71194 if (typeof callback == 'function') {
71195 callback.call(scope || me, operation);
71196 }
71197
71198 me.afterRequest(request, success);
71199 },
71200
71201 /**
71202 * @private
71203 */
71204 getResponseResult: function(response) {
71205 return response;
71206 },
71207
71208 /**
71209 * Sets up an exception on the operation
71210 * @private
71211 * @param {Ext.data.Operation} operation The operation
71212 * @param {Object} response The response
71213 */
71214 setException: function(operation, response) {
71215 if (Ext.isObject(response)) {
71216 operation.setException({
71217 status: response.status,
71218 statusText: response.statusText
71219 });
71220 }
71221 },
71222
71223 /**
71224 * Encode any values being sent to the server. Can be overridden in subclasses.
71225 * @private
71226 * @param {Array} value An array of sorters/filters.
71227 * @return {Object} The encoded value
71228 */
71229 applyEncoding: function(value) {
71230 return Ext.encode(value);
71231 },
71232
71233 /**
71234 * Encodes the array of {@link Ext.util.Sorter} objects into a string to be sent in the request url. By default,
71235 * this simply JSON-encodes the sorter data
71236 * @param {Ext.util.Sorter[]} sorters The array of {@link Ext.util.Sorter Sorter} objects
71237 * @return {String} The encoded sorters
71238 */
71239 encodeSorters: function(sorters) {
71240 var min = [],
71241 length = sorters.length,
71242 i = 0;
71243
71244 for (; i < length; i++) {
71245 min[i] = {
71246 property : sorters[i].getProperty(),
71247 direction: sorters[i].getDirection()
71248 };
71249 }
71250 return this.applyEncoding(min);
71251
71252 },
71253
71254 /**
71255 * Encodes the array of {@link Ext.util.Filter} objects into a string to be sent in the request url. By default,
71256 * this simply JSON-encodes the filter data
71257 * @param {Ext.util.Filter[]} filters The array of {@link Ext.util.Filter Filter} objects
71258 * @return {String} The encoded filters
71259 */
71260 encodeFilters: function(filters) {
71261 var min = [],
71262 length = filters.length,
71263 i = 0;
71264
71265 for (; i < length; i++) {
71266 min[i] = {
71267 property: filters[i].getProperty(),
71268 value : filters[i].getValue()
71269 };
71270 }
71271 return this.applyEncoding(min);
71272 },
71273
71274 /**
71275 * @private
71276 * Copy any sorters, filters etc into the params so they can be sent over the wire
71277 */
71278 getParams: function(operation) {
71279 var me = this,
71280 params = {},
71281 grouper = operation.getGrouper(),
71282 sorters = operation.getSorters(),
71283 filters = operation.getFilters(),
71284 page = operation.getPage(),
71285 start = operation.getStart(),
71286 limit = operation.getLimit(),
71287
71288 simpleSortMode = me.getSimpleSortMode(),
71289
71290 pageParam = me.getPageParam(),
71291 startParam = me.getStartParam(),
71292 limitParam = me.getLimitParam(),
71293 groupParam = me.getGroupParam(),
71294 sortParam = me.getSortParam(),
71295 filterParam = me.getFilterParam(),
71296 directionParam = me.getDirectionParam();
71297
71298 if (me.getEnablePagingParams()) {
71299 if (pageParam && page !== null) {
71300 params[pageParam] = page;
71301 }
71302
71303 if (startParam && start !== null) {
71304 params[startParam] = start;
71305 }
71306
71307 if (limitParam && limit !== null) {
71308 params[limitParam] = limit;
71309 }
71310 }
71311
71312 if (groupParam && grouper) {
71313 // Grouper is a subclass of sorter, so we can just use the sorter method
71314 params[groupParam] = me.encodeSorters([grouper]);
71315 }
71316
71317 if (sortParam && sorters && sorters.length > 0) {
71318 if (simpleSortMode) {
71319 params[sortParam] = sorters[0].getProperty();
71320 params[directionParam] = sorters[0].getDirection();
71321 } else {
71322 params[sortParam] = me.encodeSorters(sorters);
71323 }
71324 }
71325
71326 if (filterParam && filters && filters.length > 0) {
71327 params[filterParam] = me.encodeFilters(filters);
71328 }
71329
71330 return params;
71331 },
71332
71333 /**
71334 * Generates a url based on a given Ext.data.Request object. By default, ServerProxy's buildUrl will add the
71335 * cache-buster param to the end of the url. Subclasses may need to perform additional modifications to the url.
71336 * @param {Ext.data.Request} request The request object
71337 * @return {String} The url
71338 */
71339 buildUrl: function(request) {
71340 var me = this,
71341 url = me.getUrl(request);
71342
71343 //<debug>
71344 if (!url) {
71345 Ext.Logger.error("You are using a ServerProxy but have not supplied it with a url.");
71346 }
71347 //</debug>
71348
71349 if (me.getNoCache()) {
71350 url = Ext.urlAppend(url, Ext.String.format("{0}={1}", me.getCacheString(), Ext.Date.now()));
71351 }
71352
71353 return url;
71354 },
71355
71356 /**
71357 * Get the url for the request taking into account the order of priority,
71358 * - The request
71359 * - The api
71360 * - The url
71361 * @private
71362 * @param {Ext.data.Request} request The request
71363 * @return {String} The url
71364 */
71365 getUrl: function(request) {
71366 return request ? request.getUrl() || this.getApi()[request.getAction()] || this._url : this._url;
71367 },
71368
71369 /**
71370 * In ServerProxy subclasses, the {@link #create}, {@link #read}, {@link #update} and {@link #destroy} methods all
71371 * pass through to doRequest. Each ServerProxy subclass must implement the doRequest method - see {@link
71372 * Ext.data.proxy.JsonP} and {@link Ext.data.proxy.Ajax} for examples. This method carries the same signature as
71373 * each of the methods that delegate to it.
71374 *
71375 * @param {Ext.data.Operation} operation The Ext.data.Operation object
71376 * @param {Function} callback The callback function to call when the Operation has completed
71377 * @param {Object} scope The scope in which to execute the callback
71378 * @protected
71379 * @template
71380 */
71381 doRequest: function() {
71382 //<debug>
71383 Ext.Logger.error("The doRequest function has not been implemented on your Ext.data.proxy.Server subclass. See src/data/ServerProxy.js for details");
71384 //</debug>
71385 },
71386
71387 /**
71388 * Optional callback function which can be used to clean up after a request has been completed.
71389 * @param {Ext.data.Request} request The Request object
71390 * @param {Boolean} success True if the request was successful
71391 * @method
71392 */
71393 afterRequest: Ext.emptyFn
71394 });
71395
71396 /**
71397 * @author Ed Spencer
71398 *
71399 * AjaxProxy is one of the most widely-used ways of getting data into your application. It uses AJAX
71400 * requests to load data from the server, usually to be placed into a {@link Ext.data.Store Store}.
71401 * Let's take a look at a typical setup. Here we're going to set up a Store that has an AjaxProxy.
71402 * To prepare, we'll also set up a {@link Ext.data.Model Model}:
71403 *
71404 * Ext.define('User', {
71405 * extend: 'Ext.data.Model',
71406 * config: {
71407 * fields: ['id', 'name', 'email']
71408 * }
71409 * });
71410 *
71411 * // The Store contains the AjaxProxy as an inline configuration
71412 * var store = Ext.create('Ext.data.Store', {
71413 * model: 'User',
71414 * proxy: {
71415 * type: 'ajax',
71416 * url : 'users.json'
71417 * }
71418 * });
71419 *
71420 * store.load();
71421 *
71422 * Our example is going to load user data into a Store, so we start off by defining a
71423 * {@link Ext.data.Model Model} with the fields that we expect the server to return. Next we set up
71424 * the Store itself, along with a {@link Ext.data.Store#proxy proxy} configuration. This
71425 * configuration was automatically turned into an Ext.data.proxy.Ajax instance, with the url we
71426 * specified being passed into AjaxProxy's constructor. It's as if we'd done this:
71427 *
71428 * Ext.create('Ext.data.proxy.Ajax', {
71429 * config: {
71430 * url: 'users.json',
71431 * model: 'User',
71432 * reader: 'json'
71433 * }
71434 * });
71435 *
71436 * A couple of extra configurations appeared here - {@link #model} and {@link #reader}. These are
71437 * set by default when we create the proxy via the Store - the Store already knows about the Model,
71438 * and Proxy's default {@link Ext.data.reader.Reader Reader} is {@link Ext.data.reader.Json JsonReader}.
71439 *
71440 * Now when we call store.load(), the AjaxProxy springs into action, making a request to the url we
71441 * configured ('users.json' in this case). As we're performing a read, it sends a GET request to
71442 * that url (see {@link #actionMethods} to customize this - by default any kind of read will be sent
71443 * as a GET request and any kind of write will be sent as a POST request).
71444 *
71445 * ## Limitations
71446 *
71447 * AjaxProxy cannot be used to retrieve data from other domains. If your application is running on
71448 * http://domainA.com it cannot load data from http://domainB.com because browsers have a built-in
71449 * security policy that prohibits domains talking to each other via AJAX.
71450 *
71451 * If you need to read data from another domain and can't set up a proxy server (some software that
71452 * runs on your own domain's web server and transparently forwards requests to http://domainB.com,
71453 * making it look like they actually came from http://domainA.com), you can use
71454 * {@link Ext.data.proxy.JsonP} and a technique known as JSON-P (JSON with Padding), which can help
71455 * you get around the problem so long as the server on http://domainB.com is set up to support
71456 * JSON-P responses. See {@link Ext.data.proxy.JsonP JsonPProxy}'s introduction docs for more details.
71457 *
71458 * ## Readers and Writers
71459 *
71460 * AjaxProxy can be configured to use any type of {@link Ext.data.reader.Reader Reader} to decode
71461 * the server's response. If no Reader is supplied, AjaxProxy will default to using a
71462 * {@link Ext.data.reader.Json JsonReader}. Reader configuration can be passed in as a simple
71463 * object, which the Proxy automatically turns into a {@link Ext.data.reader.Reader Reader} instance:
71464 *
71465 * var proxy = Ext.create('Ext.data.proxy.Ajax', {
71466 * config: {
71467 * model: 'User',
71468 * reader: {
71469 * type: 'xml',
71470 * root: 'users'
71471 * }
71472 * }
71473 * });
71474 *
71475 * proxy.getReader(); //returns an {@link Ext.data.reader.Xml XmlReader} instance based on the config we supplied
71476 *
71477 * ## Url generation
71478 *
71479 * AjaxProxy automatically inserts any sorting, filtering, paging and grouping options into the url
71480 * it generates for each request. These are controlled with the following configuration options:
71481 *
71482 * - {@link #pageParam} - controls how the page number is sent to the server (see also
71483 * {@link #startParam} and {@link #limitParam})
71484 * - {@link #sortParam} - controls how sort information is sent to the server
71485 * - {@link #groupParam} - controls how grouping information is sent to the server
71486 * - {@link #filterParam} - controls how filter information is sent to the server
71487 *
71488 * Each request sent by AjaxProxy is described by an {@link Ext.data.Operation Operation}. To see
71489 * how we can customize the generated urls, let's say we're loading the Proxy with the following
71490 * Operation:
71491 *
71492 * var operation = Ext.create('Ext.data.Operation', {
71493 * action: 'read',
71494 * page : 2
71495 * });
71496 *
71497 * Now we'll issue the request for this Operation by calling {@link #read}:
71498 *
71499 * var proxy = Ext.create('Ext.data.proxy.Ajax', {
71500 * url: '/users'
71501 * });
71502 *
71503 * proxy.read(operation); // GET /users?page=2
71504 *
71505 * Easy enough - the Proxy just copied the page property from the Operation. We can customize how
71506 * this page data is sent to the server:
71507 *
71508 * var proxy = Ext.create('Ext.data.proxy.Ajax', {
71509 * url: '/users',
71510 * pageParam: 'pageNumber'
71511 * });
71512 *
71513 * proxy.read(operation); // GET /users?pageNumber=2
71514 *
71515 * Alternatively, our Operation could have been configured to send start and limit parameters
71516 * instead of page:
71517 *
71518 * var operation = Ext.create('Ext.data.Operation', {
71519 * action: 'read',
71520 * start : 50,
71521 * limit : 25
71522 * });
71523 *
71524 * var proxy = Ext.create('Ext.data.proxy.Ajax', {
71525 * url: '/users'
71526 * });
71527 *
71528 * proxy.read(operation); // GET /users?start=50&limit;=25
71529 *
71530 * Again we can customize this url:
71531 *
71532 * var proxy = Ext.create('Ext.data.proxy.Ajax', {
71533 * url: '/users',
71534 * startParam: 'startIndex',
71535 * limitParam: 'limitIndex'
71536 * });
71537 *
71538 * proxy.read(operation); // GET /users?startIndex=50&limitIndex;=25
71539 *
71540 * AjaxProxy will also send sort and filter information to the server. Let's take a look at how this
71541 * looks with a more expressive Operation object:
71542 *
71543 * var operation = Ext.create('Ext.data.Operation', {
71544 * action: 'read',
71545 * sorters: [
71546 * Ext.create('Ext.util.Sorter', {
71547 * property : 'name',
71548 * direction: 'ASC'
71549 * }),
71550 * Ext.create('Ext.util.Sorter', {
71551 * property : 'age',
71552 * direction: 'DESC'
71553 * })
71554 * ],
71555 * filters: [
71556 * Ext.create('Ext.util.Filter', {
71557 * property: 'eyeColor',
71558 * value : 'brown'
71559 * })
71560 * ]
71561 * });
71562 *
71563 * This is the type of object that is generated internally when loading a {@link Ext.data.Store Store}
71564 * with sorters and filters defined. By default the AjaxProxy will JSON encode the sorters and
71565 * filters, resulting in something like this (note that the url is escaped before sending the
71566 * request, but is left unescaped here for clarity):
71567 *
71568 * var proxy = Ext.create('Ext.data.proxy.Ajax', {
71569 * url: '/users'
71570 * });
71571 *
71572 * proxy.read(operation); // GET /users?sort=[{"property":"name","direction":"ASC"},{"property":"age","direction":"DESC"}]&filter;=[{"property":"eyeColor","value":"brown"}]
71573 *
71574 * We can again customize how this is created by supplying a few configuration options. Let's say
71575 * our server is set up to receive sorting information is a format like "sortBy=name#ASC,age#DESC".
71576 * We can configure AjaxProxy to provide that format like this:
71577 *
71578 * var proxy = Ext.create('Ext.data.proxy.Ajax', {
71579 * url: '/users',
71580 * sortParam: 'sortBy',
71581 * filterParam: 'filterBy',
71582 *
71583 * // our custom implementation of sorter encoding - turns our sorters into "name#ASC,age#DESC"
71584 * encodeSorters: function(sorters) {
71585 * var length = sorters.length,
71586 * sortStrs = [],
71587 * sorter, i;
71588 *
71589 * for (i = 0; i < length; i++) {
71590 * sorter = sorters[i];
71591 *
71592 * sortStrs[i] = sorter.property + '#' + sorter.direction;
71593 * }
71594 *
71595 * return sortStrs.join(",");
71596 * }
71597 * });
71598 *
71599 * proxy.read(operation); // GET /users?sortBy=name#ASC,age#DESC&filterBy;=[{"property":"eyeColor","value":"brown"}]
71600 *
71601 * We can also provide a custom {@link #encodeFilters} function to encode our filters.
71602 *
71603 * ###Further Reading
71604 * [Sencha Touch AJAX Guide](../../../core_concepts/using_ajax.html)
71605 * [Sencha Touch Data Overview](../../../core_concepts/data/data_package_overview.html)
71606 * [Sencha Touch Store Guide](../../../core_concepts/data/stores.html)
71607 * [Sencha Touch Models Guide](../../../core_concepts/data/models.html)
71608 * [Sencha Touch Proxy Guide](../../../core_concepts/data/proxies.html)
71609 *
71610 * @constructor
71611 * Note that if this HttpProxy is being used by a {@link Ext.data.Store Store}, then the Store's
71612 * call to {@link Ext.data.Store#method-load load} will override any specified callback and params
71613 * options. In this case, use the {@link Ext.data.Store Store}'s events to modify parameters, or
71614 * react to loading events.
71615 *
71616 * @param {Object} config (optional) Config object.
71617 * If an options parameter is passed, the singleton {@link Ext.Ajax} object will be used to
71618 * make the request.
71619 */
71620 Ext.define('Ext.data.proxy.Ajax', {
71621 extend: Ext.data.proxy.Server ,
71622
71623
71624 alias: 'proxy.ajax',
71625 alternateClassName: ['Ext.data.HttpProxy', 'Ext.data.AjaxProxy'],
71626
71627 config: {
71628 /**
71629 * @cfg {Boolean} withCredentials
71630 * This configuration is sometimes necessary when using cross-origin resource sharing.
71631 * @accessor
71632 */
71633 withCredentials: false,
71634
71635 /**
71636 * @cfg {Boolean} useDefaultXhrHeader
71637 * Set this to false to not send the default Xhr header (X-Requested-With) with every request.
71638 * This should be set to false when making CORS (cross-domain) requests.
71639 * @accessor
71640 */
71641 useDefaultXhrHeader: true,
71642
71643 /**
71644 * @cfg {String} username
71645 * Most oData feeds require basic HTTP authentication. This configuration allows
71646 * you to specify the username.
71647 * @accessor
71648 */
71649 username: null,
71650
71651 /**
71652 * @cfg {String} password
71653 * Most oData feeds require basic HTTP authentication. This configuration allows
71654 * you to specify the password.
71655 * @accessor
71656 */
71657 password: null,
71658
71659 /**
71660 * @property {Object} actionMethods
71661 * Mapping of action name to HTTP request method. In the basic AjaxProxy these are set to
71662 * 'GET' for 'read' actions and 'POST' for 'create', 'update' and 'destroy' actions.
71663 * The {@link Ext.data.proxy.Rest} maps these to the correct RESTful methods.
71664 */
71665 actionMethods: {
71666 create : 'POST',
71667 read : 'GET',
71668 update : 'POST',
71669 destroy: 'POST'
71670 },
71671
71672 /**
71673 * @cfg {Object} [headers=undefined]
71674 * Any headers to add to the Ajax request.
71675 */
71676 headers: {}
71677 },
71678
71679 /**
71680 * Performs Ajax request.
71681 * @protected
71682 * @param {Ext.data.Operation} operation
71683 * @param {Function} callback
71684 * @param {Object} scope
71685 * @return {Object}
71686 */
71687 doRequest: function(operation, callback, scope) {
71688 var me = this,
71689 writer = me.getWriter(),
71690 request = me.buildRequest(operation);
71691
71692 request.setConfig({
71693 headers: me.getHeaders(),
71694 timeout: me.getTimeout(),
71695 method: me.getMethod(request),
71696 callback: me.createRequestCallback(request, operation, callback, scope),
71697 scope: me,
71698 proxy: me,
71699 useDefaultXhrHeader: me.getUseDefaultXhrHeader()
71700 });
71701
71702 if (operation.getWithCredentials() || me.getWithCredentials()) {
71703 request.setWithCredentials(true);
71704 request.setUsername(me.getUsername());
71705 request.setPassword(me.getPassword());
71706 }
71707
71708 // We now always have the writer prepare the request
71709 request = writer.write(request);
71710
71711 Ext.Ajax.request(request.getCurrentConfig());
71712
71713 return request;
71714 },
71715
71716 /**
71717 * Returns the HTTP method name for a given request. By default this returns based on a lookup on
71718 * {@link #actionMethods}.
71719 * @param {Ext.data.Request} request The request object.
71720 * @return {String} The HTTP method to use (should be one of 'GET', 'POST', 'PUT' or 'DELETE').
71721 */
71722 getMethod: function(request) {
71723 return this.getActionMethods()[request.getAction()];
71724 },
71725
71726 /**
71727 * @private
71728 * @param {Ext.data.Request} request The Request object.
71729 * @param {Ext.data.Operation} operation The Operation being executed.
71730 * @param {Function} callback The callback function to be called when the request completes.
71731 * This is usually the callback passed to `doRequest`.
71732 * @param {Object} scope The scope in which to execute the callback function.
71733 * @return {Function} The callback function.
71734 */
71735 createRequestCallback: function(request, operation, callback, scope) {
71736 var me = this;
71737
71738 return function(options, success, response) {
71739 me.processResponse(success, operation, request, response, callback, scope);
71740 };
71741 }
71742 });
71743
71744 /**
71745 * @author Ed Spencer
71746 *
71747 * Associations enable you to express relationships between different {@link Ext.data.Model Models}. Let's say we're
71748 * writing an ecommerce system where Users can make Orders - there's a relationship between these Models that we can
71749 * express like this:
71750 *
71751 * Ext.define('MyApp.model.User', {
71752 * extend: 'Ext.data.Model',
71753 *
71754 * config: {
71755 * fields: ['id', 'name', 'email'],
71756 * hasMany: {
71757 * model: 'MyApp.model.Order',
71758 * name: 'orders'
71759 * }
71760 * }
71761 * });
71762 *
71763 * Ext.define('MyApp.model.Order', {
71764 * extend: 'Ext.data.Model',
71765 *
71766 * config: {
71767 * fields: ['id', 'user_id', 'status', 'price'],
71768 * belongsTo: 'MyApp.model.User'
71769 * }
71770 * });
71771 *
71772 * We've set up two models - User and Order - and told them about each other. You can set up as many associations on
71773 * each Model as you need using the two default types - {@link Ext.data.association.HasMany hasMany} and
71774 * {@link Ext.data.association.BelongsTo belongsTo}. There's much more detail on the usage of each of those inside their
71775 * documentation pages. If you're not familiar with Models already, {@link Ext.data.Model there is plenty on those too}.
71776 *
71777 * ## Further Reading
71778 *
71779 * - {@link Ext.data.association.HasMany hasMany associations}
71780 * - {@link Ext.data.association.BelongsTo belongsTo associations}
71781 * - {@link Ext.data.association.HasOne hasOne associations}
71782 * - {@link Ext.data.Model using Models}
71783 *
71784 * ### Self-associating Models
71785 *
71786 * We can also have models that create parent/child associations between the same type. Below is an example, where
71787 * groups can be nested inside other groups:
71788 *
71789 * // Server Data
71790 * {
71791 * "groups": {
71792 * "id": 10,
71793 * "parent_id": 100,
71794 * "name": "Main Group",
71795 * "parent_group": {
71796 * "id": 100,
71797 * "parent_id": null,
71798 * "name": "Parent Group"
71799 * },
71800 * "nested" : {
71801 * "child_groups": [{
71802 * "id": 2,
71803 * "parent_id": 10,
71804 * "name": "Child Group 1"
71805 * },{
71806 * "id": 3,
71807 * "parent_id": 10,
71808 * "name": "Child Group 2"
71809 * },{
71810 * "id": 4,
71811 * "parent_id": 10,
71812 * "name": "Child Group 3"
71813 * }]
71814 * }
71815 * }
71816 * }
71817 *
71818 * // Client code
71819 * Ext.define('MyApp.model.Group', {
71820 * extend: 'Ext.data.Model',
71821 * config: {
71822 * fields: ['id', 'parent_id', 'name'],
71823 * proxy: {
71824 * type: 'ajax',
71825 * url: 'data.json',
71826 * reader: {
71827 * type: 'json',
71828 * root: 'groups'
71829 * }
71830 * },
71831 * associations: [{
71832 * type: 'hasMany',
71833 * model: 'MyApp.model.Group',
71834 * primaryKey: 'id',
71835 * foreignKey: 'parent_id',
71836 * autoLoad: true,
71837 * associationKey: 'nested.child_groups' // read child data from nested.child_groups
71838 * }, {
71839 * type: 'belongsTo',
71840 * model: 'MyApp.model.Group',
71841 * primaryKey: 'id',
71842 * foreignKey: 'parent_id',
71843 * associationKey: 'parent_group' // read parent data from parent_group
71844 * }]
71845 * }
71846 * });
71847 *
71848 *
71849 * Ext.onReady(function(){
71850 * MyApp.model.Group.load(10, {
71851 * success: function(group){
71852 * console.log(group.getGroup().get('name'));
71853 *
71854 * group.groups().each(function(rec){
71855 * console.log(rec.get('name'));
71856 * });
71857 * }
71858 * });
71859 *
71860 * });
71861 *
71862 * ###Further Reading
71863 * [Sencha Touch Models and Associations](../../../core_concepts/data/models.html)
71864 */
71865 Ext.define('Ext.data.association.Association', {
71866 alternateClassName: 'Ext.data.Association',
71867
71868
71869
71870 config: {
71871 /**
71872 * @cfg {Ext.data.Model/String} ownerModel (required) The full class name or reference to the class that owns this
71873 * associations. This is a required configuration on every association.
71874 * @accessor
71875 */
71876 ownerModel: null,
71877
71878 /*
71879 * @cfg {String} ownerName The name for the owner model. This defaults to the last part
71880 * of the class name of the {@link #ownerModel}.
71881 */
71882 ownerName: undefined,
71883
71884 /**
71885 * @cfg {String} associatedModel (required) The full class name or reference to the class that the {@link #ownerModel}
71886 * is being associated with. This is a required configuration on every association.
71887 * @accessor
71888 */
71889 associatedModel: null,
71890
71891 /**
71892 * @cfg {String} associatedName The name for the associated model. This defaults to the last part
71893 * of the class name of the {@link #associatedModel}.
71894 * @accessor
71895 */
71896 associatedName: undefined,
71897
71898
71899 /**
71900 * @cfg {String} associationKey The name of the property in the data to read the association from.
71901 * Defaults to the {@link #associatedName} plus '_id'.
71902 */
71903 associationKey: undefined,
71904
71905 /**
71906 * @cfg {String} primaryKey The name of the primary key on the associated model.
71907 * In general this will be the {@link Ext.data.Model#idProperty} of the Model.
71908 */
71909 primaryKey: 'id',
71910
71911 /**
71912 * @cfg {Ext.data.reader.Reader} reader A special reader to read associated data.
71913 */
71914 reader: null,
71915
71916 /**
71917 * @cfg {String} type The type configuration can be used when creating associations using a configuration object.
71918 * Use `hasMany` to create a HasMany association.
71919 *
71920 * associations: [{
71921 * type: 'hasMany',
71922 * model: 'User'
71923 * }]
71924 */
71925 type: null,
71926
71927 name: undefined
71928 },
71929
71930 statics: {
71931 create: function(association) {
71932 if (!association.isAssociation) {
71933 if (Ext.isString(association)) {
71934 association = {
71935 type: association
71936 };
71937 }
71938 association.type = association.type.toLowerCase();
71939 return Ext.factory(association, Ext.data.association.Association, null, 'association');
71940 }
71941
71942 return association;
71943 }
71944 },
71945
71946 /**
71947 * Creates the Association object.
71948 * @param {Object} config (optional) Config object.
71949 */
71950 constructor: function(config) {
71951 this.initConfig(config);
71952 },
71953
71954 applyName: function(name) {
71955 if (!name) {
71956 name = this.getAssociatedName();
71957 }
71958 return name;
71959 },
71960
71961 applyOwnerModel: function(ownerName) {
71962 var ownerModel = Ext.data.ModelManager.getModel(ownerName);
71963 if (ownerModel === undefined) {
71964 Ext.Logger.error('The configured ownerModel was not valid (you tried ' + ownerName + ')');
71965 }
71966 return ownerModel;
71967 },
71968
71969 applyOwnerName: function(ownerName) {
71970 if (!ownerName) {
71971 ownerName = this.getOwnerModel().modelName;
71972 }
71973 ownerName = ownerName.slice(ownerName.lastIndexOf('.')+1);
71974 return ownerName;
71975 },
71976
71977 updateOwnerModel: function(ownerModel, oldOwnerModel) {
71978 if (oldOwnerModel) {
71979 this.setOwnerName(ownerModel.modelName);
71980 }
71981 },
71982
71983 applyAssociatedModel: function(associatedName) {
71984 var associatedModel = Ext.data.ModelManager.types[associatedName];
71985 if (associatedModel === undefined) {
71986 Ext.Logger.error('The configured associatedModel was not valid (you tried ' + associatedName + ')');
71987 }
71988 return associatedModel;
71989 },
71990
71991 applyAssociatedName: function(associatedName) {
71992 if (!associatedName) {
71993 associatedName = this.getAssociatedModel().modelName;
71994 }
71995 associatedName = associatedName.slice(associatedName.lastIndexOf('.')+1);
71996 return associatedName;
71997 },
71998
71999 updateAssociatedModel: function(associatedModel, oldAssociatedModel) {
72000 if (oldAssociatedModel) {
72001 this.setAssociatedName(associatedModel.modelName);
72002 }
72003 },
72004
72005 applyReader: function(reader) {
72006 if (reader) {
72007 if (Ext.isString(reader)) {
72008 reader = {
72009 type: reader
72010 };
72011 }
72012
72013 if (!reader.isReader) {
72014 Ext.applyIf(reader, {
72015 type: 'json'
72016 });
72017 }
72018 }
72019
72020 return Ext.factory(reader, Ext.data.Reader, this.getReader(), 'reader');
72021 },
72022
72023 updateReader: function(reader) {
72024 reader.setModel(this.getAssociatedModel());
72025 }
72026
72027 // Convert old properties in data into a config object
72028 });
72029
72030 /**
72031 * General purpose inflector class that {@link #pluralize pluralizes}, {@link #singularize singularizes} and
72032 * {@link #ordinalize ordinalizes} words. Sample usage:
72033 *
72034 * // turning singular words into plurals
72035 * Ext.util.Inflector.pluralize('word'); // 'words'
72036 * Ext.util.Inflector.pluralize('person'); // 'people'
72037 * Ext.util.Inflector.pluralize('sheep'); // 'sheep'
72038 *
72039 * // turning plurals into singulars
72040 * Ext.util.Inflector.singularize('words'); // 'word'
72041 * Ext.util.Inflector.singularize('people'); // 'person'
72042 * Ext.util.Inflector.singularize('sheep'); // 'sheep'
72043 *
72044 * // ordinalizing numbers
72045 * Ext.util.Inflector.ordinalize(11); // "11th"
72046 * Ext.util.Inflector.ordinalize(21); // "21st"
72047 * Ext.util.Inflector.ordinalize(1043); // "1043rd"
72048 *
72049 * ## Customization
72050 *
72051 * The Inflector comes with a default set of US English pluralization rules. These can be augmented with additional
72052 * rules if the default rules do not meet your application's requirements, or swapped out entirely for other languages.
72053 * Here is how we might add a rule that pluralizes "ox" to "oxen":
72054 *
72055 * Ext.util.Inflector.plural(/^(ox)$/i, "$1en");
72056 *
72057 * Each rule consists of two items - a regular expression that matches one or more rules, and a replacement string.
72058 * In this case, the regular expression will only match the string "ox", and will replace that match with "oxen".
72059 * Here's how we could add the inverse rule:
72060 *
72061 * Ext.util.Inflector.singular(/^(ox)en$/i, "$1");
72062 *
72063 * __Note:__ The ox/oxen rules are present by default.
72064 */
72065 Ext.define('Ext.util.Inflector', {
72066
72067 /* Begin Definitions */
72068
72069 singleton: true,
72070
72071 /* End Definitions */
72072
72073 /**
72074 * @private
72075 * The registered plural tuples. Each item in the array should contain two items - the first must be a regular
72076 * expression that matchers the singular form of a word, the second must be a String that replaces the matched
72077 * part of the regular expression. This is managed by the {@link #plural} method.
72078 * @property plurals
72079 * @type Array
72080 */
72081 plurals: [
72082 [(/(quiz)$/i), "$1zes" ],
72083 [(/^(ox)$/i), "$1en" ],
72084 [(/([m|l])ouse$/i), "$1ice" ],
72085 [(/(matr|vert|ind)ix|ex$/i), "$1ices" ],
72086 [(/(x|ch|ss|sh)$/i), "$1es" ],
72087 [(/([^aeiouy]|qu)y$/i), "$1ies" ],
72088 [(/(hive)$/i), "$1s" ],
72089 [(/(?:([^f])fe|([lr])f)$/i), "$1$2ves"],
72090 [(/sis$/i), "ses" ],
72091 [(/([ti])um$/i), "$1a" ],
72092 [(/(buffal|tomat|potat)o$/i), "$1oes" ],
72093 [(/(bu)s$/i), "$1ses" ],
72094 [(/(alias|status|sex)$/i), "$1es" ],
72095 [(/(octop|vir)us$/i), "$1i" ],
72096 [(/(ax|test)is$/i), "$1es" ],
72097 [(/^person$/), "people" ],
72098 [(/^man$/), "men" ],
72099 [(/^(child)$/), "$1ren" ],
72100 [(/s$/i), "s" ],
72101 [(/$/), "s" ]
72102 ],
72103
72104 /**
72105 * @private
72106 * The set of registered singular matchers. Each item in the array should contain two items - the first must be a
72107 * regular expression that matches the plural form of a word, the second must be a String that replaces the
72108 * matched part of the regular expression. This is managed by the {@link #singular} method.
72109 * @property singulars
72110 * @type Array
72111 */
72112 singulars: [
72113 [(/(quiz)zes$/i), "$1" ],
72114 [(/(matr)ices$/i), "$1ix" ],
72115 [(/(vert|ind)ices$/i), "$1ex" ],
72116 [(/^(ox)en/i), "$1" ],
72117 [(/(alias|status)es$/i), "$1" ],
72118 [(/(octop|vir)i$/i), "$1us" ],
72119 [(/(cris|ax|test)es$/i), "$1is" ],
72120 [(/(shoe)s$/i), "$1" ],
72121 [(/(o)es$/i), "$1" ],
72122 [(/(bus)es$/i), "$1" ],
72123 [(/([m|l])ice$/i), "$1ouse" ],
72124 [(/(x|ch|ss|sh)es$/i), "$1" ],
72125 [(/(m)ovies$/i), "$1ovie" ],
72126 [(/(s)eries$/i), "$1eries"],
72127 [(/([^aeiouy]|qu)ies$/i), "$1y" ],
72128 [(/([lr])ves$/i), "$1f" ],
72129 [(/(tive)s$/i), "$1" ],
72130 [(/(hive)s$/i), "$1" ],
72131 [(/([^f])ves$/i), "$1fe" ],
72132 [(/(^analy)ses$/i), "$1sis" ],
72133 [(/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i), "$1$2sis"],
72134 [(/([ti])a$/i), "$1um" ],
72135 [(/(n)ews$/i), "$1ews" ],
72136 [(/people$/i), "person" ],
72137 [(/s$/i), "" ]
72138 ],
72139
72140 /**
72141 * @private
72142 * The registered uncountable words
72143 * @property uncountable
72144 * @type Array
72145 */
72146 uncountable: [
72147 "sheep",
72148 "fish",
72149 "series",
72150 "species",
72151 "money",
72152 "rice",
72153 "information",
72154 "equipment",
72155 "grass",
72156 "mud",
72157 "offspring",
72158 "deer",
72159 "means"
72160 ],
72161
72162 /**
72163 * Adds a new singularization rule to the Inflector. See the intro docs for more information
72164 * @param {RegExp} matcher The matcher regex
72165 * @param {String} replacer The replacement string, which can reference matches from the matcher argument
72166 */
72167 singular: function(matcher, replacer) {
72168 this.singulars.unshift([matcher, replacer]);
72169 },
72170
72171 /**
72172 * Adds a new pluralization rule to the Inflector. See the intro docs for more information
72173 * @param {RegExp} matcher The matcher regex
72174 * @param {String} replacer The replacement string, which can reference matches from the matcher argument
72175 */
72176 plural: function(matcher, replacer) {
72177 this.plurals.unshift([matcher, replacer]);
72178 },
72179
72180 /**
72181 * Removes all registered singularization rules
72182 */
72183 clearSingulars: function() {
72184 this.singulars = [];
72185 },
72186
72187 /**
72188 * Removes all registered pluralization rules
72189 */
72190 clearPlurals: function() {
72191 this.plurals = [];
72192 },
72193
72194 /**
72195 * Returns true if the given word is transnumeral (the word is its own singular and plural form - e.g. sheep, fish)
72196 * @param {String} word The word to test
72197 * @return {Boolean} True if the word is transnumeral
72198 */
72199 isTransnumeral: function(word) {
72200 return Ext.Array.indexOf(this.uncountable, word) != -1;
72201 },
72202
72203 /**
72204 * Returns the pluralized form of a word (e.g. Ext.util.Inflector.pluralize('word') returns 'words')
72205 * @param {String} word The word to pluralize
72206 * @return {String} The pluralized form of the word
72207 */
72208 pluralize: function(word) {
72209 if (this.isTransnumeral(word)) {
72210 return word;
72211 }
72212
72213 var plurals = this.plurals,
72214 length = plurals.length,
72215 tuple, regex, i;
72216
72217 for (i = 0; i < length; i++) {
72218 tuple = plurals[i];
72219 regex = tuple[0];
72220
72221 if (regex == word || (regex.test && regex.test(word))) {
72222 return word.replace(regex, tuple[1]);
72223 }
72224 }
72225
72226 return word;
72227 },
72228
72229 /**
72230 * Returns the singularized form of a word (e.g. Ext.util.Inflector.singularize('words') returns 'word')
72231 * @param {String} word The word to singularize
72232 * @return {String} The singularized form of the word
72233 */
72234 singularize: function(word) {
72235 if (this.isTransnumeral(word)) {
72236 return word;
72237 }
72238
72239 var singulars = this.singulars,
72240 length = singulars.length,
72241 tuple, regex, i;
72242
72243 for (i = 0; i < length; i++) {
72244 tuple = singulars[i];
72245 regex = tuple[0];
72246
72247 if (regex == word || (regex.test && regex.test(word))) {
72248 return word.replace(regex, tuple[1]);
72249 }
72250 }
72251
72252 return word;
72253 },
72254
72255 /**
72256 * Returns the correct {@link Ext.data.Model Model} name for a given string. Mostly used internally by the data
72257 * package
72258 * @param {String} word The word to classify
72259 * @return {String} The classified version of the word
72260 */
72261 classify: function(word) {
72262 return Ext.String.capitalize(this.singularize(word));
72263 },
72264
72265 /**
72266 * Ordinalizes a given number by adding a prefix such as 'st', 'nd', 'rd' or 'th' based on the last digit of the
72267 * number. 21 -> 21st, 22 -> 22nd, 23 -> 23rd, 24 -> 24th etc
72268 * @param {Number} number The number to ordinalize
72269 * @return {String} The ordinalized number
72270 */
72271 ordinalize: function(number) {
72272 var parsed = parseInt(number, 10),
72273 mod10 = parsed % 10,
72274 mod100 = parsed % 100;
72275
72276 //11 through 13 are a special case
72277 if (11 <= mod100 && mod100 <= 13) {
72278 return number + "th";
72279 } else {
72280 switch(mod10) {
72281 case 1 : return number + "st";
72282 case 2 : return number + "nd";
72283 case 3 : return number + "rd";
72284 default: return number + "th";
72285 }
72286 }
72287 }
72288 }, function() {
72289 //aside from the rules above, there are a number of words that have irregular pluralization so we add them here
72290 var irregulars = {
72291 alumnus: 'alumni',
72292 cactus : 'cacti',
72293 focus : 'foci',
72294 nucleus: 'nuclei',
72295 radius: 'radii',
72296 stimulus: 'stimuli',
72297 ellipsis: 'ellipses',
72298 paralysis: 'paralyses',
72299 oasis: 'oases',
72300 appendix: 'appendices',
72301 index: 'indexes',
72302 beau: 'beaux',
72303 bureau: 'bureaux',
72304 tableau: 'tableaux',
72305 woman: 'women',
72306 child: 'children',
72307 man: 'men',
72308 corpus: 'corpora',
72309 criterion: 'criteria',
72310 curriculum: 'curricula',
72311 genus: 'genera',
72312 memorandum: 'memoranda',
72313 phenomenon: 'phenomena',
72314 foot: 'feet',
72315 goose: 'geese',
72316 tooth: 'teeth',
72317 antenna: 'antennae',
72318 formula: 'formulae',
72319 nebula: 'nebulae',
72320 vertebra: 'vertebrae',
72321 vita: 'vitae'
72322 },
72323 singular;
72324
72325 for (singular in irregulars) {
72326 this.plural(singular, irregulars[singular]);
72327 this.singular(irregulars[singular], singular);
72328 }
72329 });
72330
72331 /**
72332 * Represents a one-to-many relationship between two models. Usually created indirectly via a model definition:
72333 *
72334 * Ext.define('Product', {
72335 * extend: 'Ext.data.Model',
72336 * config: {
72337 * fields: [
72338 * {name: 'id', type: 'int'},
72339 * {name: 'user_id', type: 'int'},
72340 * {name: 'name', type: 'string'}
72341 * ]
72342 * }
72343 * });
72344 *
72345 * Ext.define('User', {
72346 * extend: 'Ext.data.Model',
72347 * config: {
72348 * fields: [
72349 * {name: 'id', type: 'int'},
72350 * {name: 'name', type: 'string'}
72351 * ],
72352 * // we can use the hasMany shortcut on the model to create a hasMany association
72353 * hasMany: {model: 'Product', name: 'products'}
72354 * }
72355 * });
72356 *
72357 * Above we created Product and User models, and linked them by saying that a User hasMany Products. This gives us a new
72358 * function on every User instance, in this case the function is called 'products' because that is the name we specified
72359 * in the association configuration above.
72360 *
72361 * This new function returns a specialized {@link Ext.data.Store Store} which is automatically filtered to load only
72362 * Products for the given model instance:
72363 *
72364 * //first, we load up a User with id of 1
72365 * var user = Ext.create('User', {id: 1, name: 'Ed'});
72366 *
72367 * //the user.products function was created automatically by the association and returns a {@link Ext.data.Store Store}
72368 * //the created store is automatically scoped to the set of Products for the User with id of 1
72369 * var products = user.products();
72370 *
72371 * //we still have all of the usual Store functions, for example it's easy to add a Product for this User
72372 * products.add({
72373 * name: 'Another Product'
72374 * });
72375 *
72376 * //saves the changes to the store - this automatically sets the new Product's user_id to 1 before saving
72377 * products.sync();
72378 *
72379 * The new Store is only instantiated the first time you call products() to conserve memory and processing time, though
72380 * calling products() a second time returns the same store instance.
72381 *
72382 * _Custom filtering_
72383 *
72384 * The Store is automatically furnished with a filter - by default this filter tells the store to only return records
72385 * where the associated model's foreign key matches the owner model's primary key. For example, if a User with ID = 100
72386 * hasMany Products, the filter loads only Products with user_id == 100.
72387 *
72388 * Sometimes we want to filter by another field - for example in the case of a Twitter search application we may have
72389 * models for Search and Tweet:
72390 *
72391 * Ext.define('Search', {
72392 * extend: 'Ext.data.Model',
72393 * config: {
72394 * fields: [
72395 * 'id', 'query'
72396 * ],
72397 *
72398 * hasMany: {
72399 * model: 'Tweet',
72400 * name : 'tweets',
72401 * filterProperty: 'query'
72402 * }
72403 * }
72404 * });
72405 *
72406 * Ext.define('Tweet', {
72407 * extend: 'Ext.data.Model',
72408 * config: {
72409 * fields: [
72410 * 'id', 'text', 'from_user'
72411 * ]
72412 * }
72413 * });
72414 *
72415 * //returns a Store filtered by the filterProperty
72416 * var store = new Search({query: 'Sencha Touch'}).tweets();
72417 *
72418 * The tweets association above is filtered by the query property by setting the {@link #filterProperty}, and is
72419 * equivalent to this:
72420 *
72421 * var store = Ext.create('Ext.data.Store', {
72422 * model: 'Tweet',
72423 * filters: [
72424 * {
72425 * property: 'query',
72426 * value : 'Sencha Touch'
72427 * }
72428 * ]
72429 * });
72430 *
72431 * ###Further Reading
72432 * [Sencha Touch Models and Associations](../../../core_concepts/data/models.html)
72433 */
72434 Ext.define('Ext.data.association.HasMany', {
72435 extend: Ext.data.association.Association ,
72436 alternateClassName: 'Ext.data.HasManyAssociation',
72437
72438
72439 alias: 'association.hasmany',
72440
72441 config: {
72442 /**
72443 * @cfg {String} foreignKey
72444 * The name of the foreign key on the associated model that links it to the owner model. Defaults to the
72445 * lowercased name of the owner model plus "_id", e.g. an association with a model called Group hasMany Users
72446 * would create 'group_id' as the foreign key. When the remote store is loaded, the store is automatically
72447 * filtered so that only records with a matching foreign key are included in the resulting child store. This can
72448 * be overridden by specifying the {@link #filterProperty}.
72449 *
72450 * Ext.define('Group', {
72451 * extend: 'Ext.data.Model',
72452 * fields: ['id', 'name'],
72453 * hasMany: 'User'
72454 * });
72455 *
72456 * Ext.define('User', {
72457 * extend: 'Ext.data.Model',
72458 * fields: ['id', 'name', 'group_id'], // refers to the id of the group that this user belongs to
72459 * belongsTo: 'Group'
72460 * });
72461 */
72462 foreignKey: undefined,
72463
72464 /**
72465 * @cfg {String} name
72466 * The name of the function to create on the owner model to retrieve the child store. If not specified, the
72467 * pluralized name of the child model is used.
72468 *
72469 * // This will create a users() method on any Group model instance
72470 * Ext.define('Group', {
72471 * extend: 'Ext.data.Model',
72472 * fields: ['id', 'name'],
72473 * hasMany: 'User'
72474 * });
72475 * var group = new Group();
72476 * console.log(group.users());
72477 *
72478 * // The method to retrieve the users will now be getUserList
72479 * Ext.define('Group', {
72480 * extend: 'Ext.data.Model',
72481 * fields: ['id', 'name'],
72482 * hasMany: {model: 'User', name: 'getUserList'}
72483 * });
72484 * var group = new Group();
72485 * console.log(group.getUserList());
72486 */
72487
72488 /**
72489 * @cfg {Object} store
72490 * Optional configuration object that will be passed to the generated Store. Defaults to an empty Object.
72491 */
72492 store: undefined,
72493
72494 /**
72495 * @cfg {String} storeName
72496 * Optional The name of the store by which you can reference it on this class as a property.
72497 */
72498 storeName: undefined,
72499
72500 /**
72501 * @cfg {String} filterProperty
72502 * Optionally overrides the default filter that is set up on the associated Store. If this is not set, a filter
72503 * is automatically created which filters the association based on the configured {@link #foreignKey}. See intro
72504 * docs for more details.
72505 */
72506 filterProperty: null,
72507
72508 /**
72509 * @cfg {Boolean} autoLoad
72510 * `true` to automatically load the related store from a remote source when instantiated.
72511 */
72512 autoLoad: false,
72513
72514 /**
72515 * @cfg {Boolean} autoSync
72516 * true to automatically synchronize the related store with the remote source
72517 */
72518 autoSync: false
72519 },
72520
72521 constructor: function(config) {
72522 config = config || {};
72523
72524 if (config.storeConfig) {
72525 // <debug>
72526 Ext.Logger.warn('storeConfig is deprecated on an association. Instead use the store configuration.');
72527 // </debug>
72528 config.store = config.storeConfig;
72529 delete config.storeConfig;
72530 }
72531
72532 this.callParent([config]);
72533 },
72534
72535 applyName: function(name) {
72536 if (!name) {
72537 name = Ext.util.Inflector.pluralize(this.getAssociatedName().toLowerCase());
72538 }
72539 return name;
72540 },
72541
72542 applyStoreName: function(name) {
72543 if (!name) {
72544 name = this.getName() + 'Store';
72545 }
72546 return name;
72547 },
72548
72549 applyForeignKey: function(foreignKey) {
72550 if (!foreignKey) {
72551 var inverse = this.getInverseAssociation();
72552 if (inverse) {
72553 foreignKey = inverse.getForeignKey();
72554 } else {
72555 foreignKey = this.getOwnerName().toLowerCase() + '_id';
72556 }
72557 }
72558 return foreignKey;
72559 },
72560
72561 applyAssociationKey: function(associationKey) {
72562 if (!associationKey) {
72563 var associatedName = this.getAssociatedName();
72564 associationKey = Ext.util.Inflector.pluralize(associatedName[0].toLowerCase() + associatedName.slice(1));
72565 }
72566 return associationKey;
72567 },
72568
72569 updateForeignKey: function(foreignKey, oldForeignKey) {
72570 var fields = this.getAssociatedModel().getFields(),
72571 field = fields.get(foreignKey);
72572
72573 if (!field) {
72574 field = new Ext.data.Field({
72575 name: foreignKey
72576 });
72577 fields.add(field);
72578 fields.isDirty = true;
72579 }
72580
72581 if (oldForeignKey) {
72582 field = fields.get(oldForeignKey);
72583 if (field) {
72584 fields.remove(field);
72585 fields.isDirty = true;
72586 }
72587 }
72588 },
72589
72590 /**
72591 * @private
72592 * @deprecated as of v2.0.0 on an association. Instead use the store configuration.
72593 *
72594 * Creates a function that returns an Ext.data.Store which is configured to load a set of data filtered
72595 * by the owner model's primary key - e.g. in a `hasMany` association where Group `hasMany` Users, this function
72596 * returns a Store configured to return the filtered set of a single Group's Users.
72597 * @return {Function} The store-generating function.
72598 */
72599 applyStore: function(storeConfig) {
72600 var me = this,
72601 associatedModel = me.getAssociatedModel(),
72602 storeName = me.getStoreName(),
72603 foreignKey = me.getForeignKey(),
72604 primaryKey = me.getPrimaryKey(),
72605 filterProperty = me.getFilterProperty(),
72606 autoLoad = me.getAutoLoad(),
72607 autoSync = me.getAutoSync();
72608
72609 return function() {
72610 var record = this,
72611 config, filter, store,
72612 modelDefaults = {},
72613 listeners = {
72614 addrecords: me.onAddRecords,
72615 removerecords: me.onRemoveRecords,
72616 scope: me
72617 };
72618
72619 if (record[storeName] === undefined) {
72620 if (filterProperty) {
72621 filter = {
72622 property : filterProperty,
72623 value : record.get(filterProperty),
72624 exactMatch: true
72625 };
72626 } else {
72627 filter = {
72628 property : foreignKey,
72629 value : record.get(primaryKey),
72630 exactMatch: true
72631 };
72632 }
72633
72634 modelDefaults[foreignKey] = record.get(primaryKey);
72635
72636 config = Ext.apply({}, storeConfig, {
72637 model : associatedModel,
72638 filters : [filter],
72639 remoteFilter : true,
72640 autoSync : autoSync,
72641 modelDefaults: modelDefaults
72642 });
72643
72644 store = record[storeName] = Ext.create('Ext.data.Store', config);
72645 store.boundTo = record;
72646
72647 store.onAfter(listeners);
72648
72649 if (autoLoad) {
72650 record[storeName].load();
72651 }
72652 }
72653
72654 return record[storeName];
72655 };
72656 },
72657
72658 onAddRecords: function(store, records) {
72659 var ln = records.length,
72660 id = store.boundTo.getId(),
72661 i, record;
72662
72663 for (i = 0; i < ln; i++) {
72664 record = records[i];
72665 record.set(this.getForeignKey(), id);
72666 }
72667 this.updateInverseInstances(store.boundTo);
72668 },
72669
72670 onRemoveRecords: function(store, records) {
72671 var ln = records.length,
72672 i, record;
72673 for (i = 0; i < ln; i++) {
72674 record = records[i];
72675 record.set(this.getForeignKey(), null);
72676 }
72677 },
72678
72679 updateStore: function(store) {
72680 this.getOwnerModel().prototype[this.getName()] = store;
72681 },
72682
72683 /**
72684 * Read associated data
72685 * @private
72686 * @param {Ext.data.Model} record The record we're writing to.
72687 * @param {Ext.data.reader.Reader} reader The reader for the associated model.
72688 * @param {Object} associationData The raw associated data.
72689 */
72690 read: function(record, reader, associationData) {
72691 var store = record[this.getName()](),
72692 records = reader.read(associationData).getRecords();
72693
72694 store.add(records);
72695 },
72696
72697 updateInverseInstances: function(record) {
72698 var store = record[this.getName()](),
72699 inverse = this.getInverseAssociation();
72700
72701 //if the inverse association was found, set it now on each record we've just created
72702 if (inverse) {
72703 store.each(function(associatedRecord) {
72704 associatedRecord[inverse.getInstanceName()] = record;
72705 });
72706 }
72707 },
72708
72709 getInverseAssociation: function() {
72710 var ownerName = this.getOwnerModel().modelName;
72711
72712 //now that we've added the related records to the hasMany association, set the inverse belongsTo
72713 //association on each of them if it exists
72714 return this.getAssociatedModel().associations.findBy(function(assoc) {
72715 return assoc.getType().toLowerCase() === 'belongsto' && assoc.getAssociatedModel().modelName === ownerName;
72716 });
72717 }
72718
72719 });
72720
72721 /**
72722 * @author Ed Spencer
72723 *
72724 * Represents a many to one association with another model. The owner model is expected to have
72725 * a foreign key which references the primary key of the associated model:
72726 *
72727 * Ext.define('Category', {
72728 * extend: 'Ext.data.Model',
72729 * config: {
72730 * fields: [
72731 * { name: 'id', type: 'int' },
72732 * { name: 'name', type: 'string' }
72733 * ]
72734 * }
72735 * });
72736 *
72737 * Ext.define('Product', {
72738 * extend: 'Ext.data.Model',
72739 * config: {
72740 * fields: [
72741 * { name: 'id', type: 'int' },
72742 * { name: 'category_id', type: 'int' },
72743 * { name: 'name', type: 'string' }
72744 * ],
72745 * // we can use the belongsTo shortcut on the model to create a belongsTo association
72746 * belongsTo: {
72747 * model: 'Category'
72748 * }
72749 * }
72750 * });
72751 *
72752 * In the example above we have created models for Products and Categories, and linked them together
72753 * by saying that each Product belongs to a Category. This automatically links each Product to a Category
72754 * based on the Product's category_id, and provides new functions on the Product model:
72755 *
72756 * ## Generated getter function
72757 *
72758 * The first function that is added to the owner model is a getter function:
72759 *
72760 * var product = new Product({
72761 * id: 100,
72762 * category_id: 20,
72763 * name: 'Sneakers'
72764 * });
72765 *
72766 * product.getCategory(function(category, operation) {
72767 * // do something with the category object
72768 * alert(category.get('id')); // alerts 20
72769 * }, this);
72770 *
72771 * The getCategory function was created on the Product model when we defined the association. This uses the
72772 * Category's configured {@link Ext.data.proxy.Proxy proxy} to load the Category asynchronously, calling the provided
72773 * callback when it has loaded.
72774 *
72775 * The new getCategory function will also accept an object containing success, failure and callback properties
72776 * - callback will always be called, success will only be called if the associated model was loaded successfully
72777 * and failure will only be called if the associated model could not be loaded:
72778 *
72779 * product.getCategory({
72780 * reload: true, // force a reload if the owner model is already cached
72781 * callback: function(category, operation) {}, // a function that will always be called
72782 * success : function(category, operation) {}, // a function that will only be called if the load succeeded
72783 * failure : function(category, operation) {}, // a function that will only be called if the load did not succeed
72784 * scope : this // optionally pass in a scope object to execute the callbacks in
72785 * });
72786 *
72787 * In each case above the callbacks are called with two arguments - the associated model instance and the
72788 * {@link Ext.data.Operation operation} object that was executed to load that instance. The Operation object is
72789 * useful when the instance could not be loaded.
72790 *
72791 * Once the getter has been called on the model, it will be cached if the getter is called a second time. To
72792 * force the model to reload, specify reload: true in the options object.
72793 *
72794 * ## Generated setter function
72795 *
72796 * The second generated function sets the associated model instance - if only a single argument is passed to
72797 * the setter then the following two calls are identical:
72798 *
72799 * // this call...
72800 * product.setCategory(10);
72801 *
72802 * // is equivalent to this call:
72803 * product.set('category_id', 10);
72804 *
72805 * An instance of the owner model can also be passed as a parameter.
72806 *
72807 * If we pass in a second argument, the model will be automatically saved and the second argument passed to
72808 * the owner model's {@link Ext.data.Model#save save} method:
72809 *
72810 * product.setCategory(10, function(product, operation) {
72811 * // the product has been saved
72812 * alert(product.get('category_id')); //now alerts 10
72813 * });
72814 *
72815 * //alternative syntax:
72816 * product.setCategory(10, {
72817 * callback: function(product, operation) {}, // a function that will always be called
72818 * success : function(product, operation) {}, // a function that will only be called if the load succeeded
72819 * failure : function(product, operation) {}, // a function that will only be called if the load did not succeed
72820 * scope : this //optionally pass in a scope object to execute the callbacks in
72821 * });
72822 *
72823 * ## Customization
72824 *
72825 * Associations reflect on the models they are linking to automatically set up properties such as the
72826 * {@link #primaryKey} and {@link #foreignKey}. These can alternatively be specified:
72827 *
72828 * Ext.define('Product', {
72829 * extend: 'Ext.data.Model',
72830 * config: {
72831 * fields: [
72832 * // ...
72833 * ],
72834 *
72835 * belongsTo: {
72836 * model : 'Category',
72837 * primaryKey: 'unique_key',
72838 * foreignKey: 'cat_id'
72839 * }
72840 * }
72841 * });
72842 *
72843 * Here we replaced the default primary key (defaults to 'id') and foreign key (calculated as 'category_id')
72844 * with our own settings. Usually this will not be needed.
72845 *
72846 * ###Further Reading
72847 * [Sencha Touch Models and Associations](../../../core_concepts/data/models.html)
72848 */
72849 Ext.define('Ext.data.association.BelongsTo', {
72850 extend: Ext.data.association.Association ,
72851 alternateClassName: 'Ext.data.BelongsToAssociation',
72852 alias: 'association.belongsto',
72853
72854 config: {
72855 /**
72856 * @cfg {String} foreignKey The name of the foreign key on the owner model that links it to the associated
72857 * model. Defaults to the lowercased name of the associated model plus "_id", e.g. an association with a
72858 * model called Product would set up a product_id foreign key.
72859 *
72860 * Ext.define('Order', {
72861 * extend: 'Ext.data.Model',
72862 * fields: ['id', 'date'],
72863 * hasMany: 'Product'
72864 * });
72865 *
72866 * Ext.define('Product', {
72867 * extend: 'Ext.data.Model',
72868 * fields: ['id', 'name', 'order_id'], // refers to the id of the order that this product belongs to
72869 * belongsTo: 'Group'
72870 * });
72871 * var product = new Product({
72872 * id: 1,
72873 * name: 'Product 1',
72874 * order_id: 22
72875 * }, 1);
72876 * product.getOrder(); // Will make a call to the server asking for order_id 22
72877 *
72878 */
72879 foreignKey: undefined,
72880
72881 /**
72882 * @cfg {String} getterName The name of the getter function that will be added to the local model's prototype.
72883 * Defaults to 'get' + the name of the foreign model, e.g. getCategory
72884 */
72885 getterName: undefined,
72886
72887 /**
72888 * @cfg {String} setterName The name of the setter function that will be added to the local model's prototype.
72889 * Defaults to 'set' + the name of the foreign model, e.g. setCategory
72890 */
72891 setterName: undefined,
72892
72893 instanceName: undefined
72894 },
72895
72896 applyForeignKey: function(foreignKey) {
72897 if (!foreignKey) {
72898 foreignKey = this.getAssociatedName().toLowerCase() + '_id';
72899 }
72900 return foreignKey;
72901 },
72902
72903 updateForeignKey: function(foreignKey, oldForeignKey) {
72904 var fields = this.getOwnerModel().getFields(),
72905 field = fields.get(foreignKey);
72906
72907 if (!field) {
72908 field = new Ext.data.Field({
72909 name: foreignKey
72910 });
72911 fields.add(field);
72912 fields.isDirty = true;
72913 }
72914
72915 if (oldForeignKey) {
72916 field = fields.get(oldForeignKey);
72917 if (field) {
72918 fields.isDirty = true;
72919 fields.remove(field);
72920 }
72921 }
72922 },
72923
72924 applyInstanceName: function(instanceName) {
72925 if (!instanceName) {
72926 instanceName = this.getAssociatedName() + 'BelongsToInstance';
72927 }
72928 return instanceName;
72929 },
72930
72931 applyAssociationKey: function(associationKey) {
72932 if (!associationKey) {
72933 var associatedName = this.getAssociatedName();
72934 associationKey = associatedName[0].toLowerCase() + associatedName.slice(1);
72935 }
72936 return associationKey;
72937 },
72938
72939 applyGetterName: function(getterName) {
72940 if (!getterName) {
72941 var associatedName = this.getAssociatedName();
72942 getterName = 'get' + associatedName[0].toUpperCase() + associatedName.slice(1);
72943 }
72944 return getterName;
72945 },
72946
72947 applySetterName: function(setterName) {
72948 if (!setterName) {
72949 var associatedName = this.getAssociatedName();
72950 setterName = 'set' + associatedName[0].toUpperCase() + associatedName.slice(1);
72951 }
72952 return setterName;
72953 },
72954
72955 updateGetterName: function(getterName, oldGetterName) {
72956 var ownerProto = this.getOwnerModel().prototype;
72957 if (oldGetterName) {
72958 delete ownerProto[oldGetterName];
72959 }
72960 if (getterName) {
72961 ownerProto[getterName] = this.createGetter();
72962 }
72963 },
72964
72965 updateSetterName: function(setterName, oldSetterName) {
72966 var ownerProto = this.getOwnerModel().prototype;
72967 if (oldSetterName) {
72968 delete ownerProto[oldSetterName];
72969 }
72970 if (setterName) {
72971 ownerProto[setterName] = this.createSetter();
72972 }
72973 },
72974
72975 /**
72976 * @private
72977 * Returns a setter function to be placed on the owner model's prototype
72978 * @return {Function} The setter function
72979 */
72980 createSetter: function() {
72981 var me = this,
72982 foreignKey = me.getForeignKey(),
72983 associatedModel = me.getAssociatedModel(),
72984 currentOwner, newOwner, store;
72985
72986 //'this' refers to the Model instance inside this function
72987 return function(value, options, scope) {
72988 var inverse = me.getInverseAssociation(),
72989 record = this;
72990
72991 // If we pass in an instance, pull the id out
72992 if (value && value.isModel) {
72993 value = value.getId();
72994 }
72995
72996 if (Ext.isFunction(options)) {
72997 options = {
72998 callback: options,
72999 scope: scope || record
73000 };
73001 }
73002
73003 // Remove the current belongsToInstance
73004 delete record[me.getInstanceName()];
73005
73006 currentOwner = Ext.data.Model.cache[Ext.data.Model.generateCacheId(associatedModel.modelName, this.get(foreignKey))];
73007 newOwner = Ext.data.Model.cache[Ext.data.Model.generateCacheId(associatedModel.modelName, value)];
73008
73009 record.set(foreignKey, value);
73010
73011 if (inverse) {
73012 // We first add it to the new owner so that the record wouldnt be destroyed if it was the last store it was in
73013 if (newOwner) {
73014 if (inverse.getType().toLowerCase() === 'hasmany') {
73015 store = newOwner[inverse.getName()]();
73016 store.add(record);
73017 } else {
73018 newOwner[inverse.getInstanceName()] = record;
73019 }
73020 }
73021
73022 if (currentOwner) {
73023 if (inverse.getType().toLowerCase() === 'hasmany') {
73024 store = currentOwner[inverse.getName()]();
73025 store.remove(record);
73026 } else {
73027 delete value[inverse.getInstanceName()];
73028 }
73029 }
73030 }
73031
73032 if (newOwner) {
73033 record[me.getInstanceName()] = newOwner;
73034 }
73035
73036 if (Ext.isObject(options)) {
73037 return record.save(options);
73038 }
73039
73040 return record;
73041 };
73042 },
73043
73044 /**
73045 * @private
73046 * Returns a getter function to be placed on the owner model's prototype. We cache the loaded instance
73047 * the first time it is loaded so that subsequent calls to the getter always receive the same reference.
73048 * @return {Function} The getter function
73049 */
73050 createGetter: function() {
73051 var me = this,
73052 associatedModel = me.getAssociatedModel(),
73053 foreignKey = me.getForeignKey(),
73054 instanceName = me.getInstanceName();
73055
73056 //'this' refers to the Model instance inside this function
73057 return function(options, scope) {
73058 options = options || {};
73059
73060 var model = this,
73061 foreignKeyId = model.get(foreignKey),
73062 success,
73063 instance,
73064 args;
73065
73066 instance = model[instanceName];
73067
73068 if (!instance) {
73069 instance = Ext.data.Model.cache[Ext.data.Model.generateCacheId(associatedModel.modelName, foreignKeyId)];
73070 if (instance) {
73071 model[instanceName] = instance;
73072 }
73073 }
73074
73075 if (options.reload === true || instance === undefined) {
73076 if (typeof options == 'function') {
73077 options = {
73078 callback: options,
73079 scope: scope || model
73080 };
73081 }
73082
73083 // Overwrite the success handler so we can assign the current instance
73084 success = options.success;
73085 options.success = function(rec) {
73086 model[instanceName] = rec;
73087 if (success) {
73088 success.apply(this, arguments);
73089 }
73090 };
73091
73092 associatedModel.load(foreignKeyId, options);
73093 } else {
73094 args = [instance];
73095 scope = scope || model;
73096
73097 Ext.callback(options, scope, args);
73098 Ext.callback(options.success, scope, args);
73099 Ext.callback(options.failure, scope, args);
73100 Ext.callback(options.callback, scope, args);
73101
73102 return instance;
73103 }
73104 };
73105 },
73106
73107 /**
73108 * Read associated data
73109 * @private
73110 * @param {Ext.data.Model} record The record we're writing to
73111 * @param {Ext.data.reader.Reader} reader The reader for the associated model
73112 * @param {Object} associationData The raw associated data
73113 */
73114 read: function(record, reader, associationData){
73115 record[this.getInstanceName()] = reader.read([associationData]).getRecords()[0];
73116 },
73117
73118 getInverseAssociation: function() {
73119 var ownerName = this.getOwnerModel().modelName,
73120 foreignKey = this.getForeignKey();
73121
73122 return this.getAssociatedModel().associations.findBy(function(assoc) {
73123 var type = assoc.getType().toLowerCase();
73124 return (type === 'hasmany' || type === 'hasone') && assoc.getAssociatedModel().modelName === ownerName && assoc.getForeignKey() === foreignKey;
73125 });
73126 }
73127 });
73128
73129 /**
73130 * Represents a one to one association with another model. The owner model is expected to have
73131 * a foreign key which references the primary key of the associated model:
73132 *
73133 * Ext.define('Person', {
73134 * extend: 'Ext.data.Model',
73135 * config: {
73136 * fields: [
73137 * { name: 'id', type: 'int' },
73138 * { name: 'name', type: 'string' },
73139 * { name: 'address_id', type: 'int'}
73140 * ],
73141 *
73142 * // we can use the hasOne shortcut on the model to create a hasOne association
73143 * hasOne: {
73144 * model: 'Address',
73145 * name : 'address'
73146 * }
73147 * }
73148 * });
73149 *
73150 * Ext.define('Address', {
73151 * extend: 'Ext.data.Model',
73152 * config: {
73153 * fields: [
73154 * { name: 'id', type: 'int' },
73155 * { name: 'number', type: 'string' },
73156 * { name: 'street', type: 'string' },
73157 * { name: 'city', type: 'string' },
73158 * { name: 'zip', type: 'string' }
73159 * ]
73160 * }
73161 * });
73162 *
73163 * In the example above we have created models for People and Addresses, and linked them together
73164 * by saying that each Person has a single Address. This automatically links each Person to an Address
73165 * based on the Persons address_id, and provides new functions on the Person model:
73166 *
73167 * ## Generated getter function
73168 *
73169 * The first function that is added to the owner model is a getter function:
73170 *
73171 * var person = Ext.create('Person', {
73172 * id: 100,
73173 * address_id: 20,
73174 * name: 'John Smith'
73175 * });
73176 *
73177 * person.getAddress(function(address, operation) {
73178 * // do something with the address object
73179 * alert(address.get('id')); // alerts 20
73180 * }, this);
73181 *
73182 * The getAddress function was created on the Person model when we defined the association. This uses the
73183 * Persons configured {@link Ext.data.proxy.Proxy proxy} to load the Address asynchronously, calling the provided
73184 * callback when it has loaded.
73185 *
73186 * The new getAddress function will also accept an object containing success, failure and callback properties
73187 * - callback will always be called, success will only be called if the associated model was loaded successfully
73188 * and failure will only be called if the associated model could not be loaded:
73189 *
73190 * person.getAddress({
73191 * reload: true, // force a reload if the owner model is already cached
73192 * callback: function(address, operation) {}, // a function that will always be called
73193 * success : function(address, operation) {}, // a function that will only be called if the load succeeded
73194 * failure : function(address, operation) {}, // a function that will only be called if the load did not succeed
73195 * scope : this // optionally pass in a scope object to execute the callbacks in
73196 * });
73197 *
73198 * In each case above the callbacks are called with two arguments - the associated model instance and the
73199 * {@link Ext.data.Operation operation} object that was executed to load that instance. The Operation object is
73200 * useful when the instance could not be loaded.
73201 *
73202 * Once the getter has been called on the model, it will be cached if the getter is called a second time. To
73203 * force the model to reload, specify reload: true in the options object.
73204 *
73205 * ## Generated setter function
73206 *
73207 * The second generated function sets the associated model instance - if only a single argument is passed to
73208 * the setter then the following two calls are identical:
73209 *
73210 * // this call...
73211 * person.setAddress(10);
73212 *
73213 * // is equivalent to this call:
73214 * person.set('address_id', 10);
73215 *
73216 * An instance of the owner model can also be passed as a parameter.
73217 *
73218 * If we pass in a second argument, the model will be automatically saved and the second argument passed to
73219 * the owner model's {@link Ext.data.Model#save save} method:
73220 *
73221 * person.setAddress(10, function(address, operation) {
73222 * // the address has been saved
73223 * alert(address.get('address_id')); //now alerts 10
73224 * });
73225 *
73226 * //alternative syntax:
73227 * person.setAddress(10, {
73228 * callback: function(address, operation) {}, // a function that will always be called
73229 * success : function(address, operation) {}, // a function that will only be called if the load succeeded
73230 * failure : function(address, operation) {}, // a function that will only be called if the load did not succeed
73231 * scope : this //optionally pass in a scope object to execute the callbacks in
73232 * });
73233 *
73234 * ## Customization
73235 *
73236 * Associations reflect on the models they are linking to automatically set up properties such as the
73237 * {@link #primaryKey} and {@link #foreignKey}. These can alternatively be specified:
73238 *
73239 * Ext.define('Person', {
73240 * extend: 'Ext.data.Model',
73241 * config: {
73242 * fields: [
73243 * // ...
73244 * ],
73245 *
73246 * hasOne: {
73247 * model : 'Address',
73248 * primaryKey: 'unique_id',
73249 * foreignKey: 'addr_id'
73250 * }
73251 * }
73252 * });
73253 *
73254 * Here we replaced the default primary key (defaults to 'id') and foreign key (calculated as 'address_id')
73255 * with our own settings. Usually this will not be needed.
73256 *
73257 * ###Further Reading
73258 * [Sencha Touch Models and Associations](../../../core_concepts/data/models.html)
73259 */
73260 Ext.define('Ext.data.association.HasOne', {
73261 extend: Ext.data.association.Association ,
73262 alternateClassName: 'Ext.data.HasOneAssociation',
73263
73264 alias: 'association.hasone',
73265
73266 config: {
73267 /**
73268 * @cfg {String} foreignKey The name of the foreign key on the owner model that links it to the associated
73269 * model. Defaults to the lowercased name of the associated model plus "_id", e.g. an association with a
73270 * model called Person would set up a address_id foreign key.
73271 *
73272 * Ext.define('Person', {
73273 * extend: 'Ext.data.Model',
73274 * fields: ['id', 'name', 'address_id'], // refers to the id of the address object
73275 * hasOne: 'Address'
73276 * });
73277 *
73278 * Ext.define('Address', {
73279 * extend: 'Ext.data.Model',
73280 * fields: ['id', 'number', 'street', 'city', 'zip'],
73281 * belongsTo: 'Person'
73282 * });
73283 * var Person = new Person({
73284 * id: 1,
73285 * name: 'John Smith',
73286 * address_id: 13
73287 * }, 1);
73288 * person.getAddress(); // Will make a call to the server asking for address_id 13
73289 *
73290 */
73291 foreignKey: undefined,
73292
73293 /**
73294 * @cfg {String} getterName The name of the getter function that will be added to the local model's prototype.
73295 * Defaults to 'get' + the name of the foreign model, e.g. getAddress
73296 */
73297 getterName: undefined,
73298
73299 /**
73300 * @cfg {String} setterName The name of the setter function that will be added to the local model's prototype.
73301 * Defaults to 'set' + the name of the foreign model, e.g. setAddress
73302 */
73303 setterName: undefined,
73304
73305 instanceName: undefined
73306 },
73307
73308 applyForeignKey: function(foreignKey) {
73309 if (!foreignKey) {
73310 var inverse = this.getInverseAssociation();
73311 if (inverse) {
73312 foreignKey = inverse.getForeignKey();
73313 } else {
73314 foreignKey = this.getAssociatedName().toLowerCase() + '_id';
73315 }
73316 }
73317 return foreignKey;
73318 },
73319
73320 updateForeignKey: function(foreignKey, oldForeignKey) {
73321 var fields = this.getOwnerModel().getFields(),
73322 field = fields.get(foreignKey);
73323
73324 if (!field) {
73325 field = new Ext.data.Field({
73326 name: foreignKey
73327 });
73328 fields.add(field);
73329 fields.isDirty = true;
73330 }
73331
73332 if (oldForeignKey) {
73333 field = fields.get(oldForeignKey);
73334 if (field) {
73335 fields.remove(field);
73336 fields.isDirty = true;
73337 }
73338 }
73339 },
73340
73341 applyInstanceName: function(instanceName) {
73342 if (!instanceName) {
73343 instanceName = this.getAssociatedName() + 'HasOneInstance';
73344 }
73345 return instanceName;
73346 },
73347
73348 applyAssociationKey: function(associationKey) {
73349 if (!associationKey) {
73350 var associatedName = this.getAssociatedName();
73351 associationKey = associatedName[0].toLowerCase() + associatedName.slice(1);
73352 }
73353 return associationKey;
73354 },
73355
73356 applyGetterName: function(getterName) {
73357 if (!getterName) {
73358 var associatedName = this.getAssociatedName();
73359 getterName = 'get' + associatedName[0].toUpperCase() + associatedName.slice(1);
73360 }
73361 return getterName;
73362 },
73363
73364 applySetterName: function(setterName) {
73365 if (!setterName) {
73366 var associatedName = this.getAssociatedName();
73367 setterName = 'set' + associatedName[0].toUpperCase() + associatedName.slice(1);
73368 }
73369 return setterName;
73370 },
73371
73372 updateGetterName: function(getterName, oldGetterName) {
73373 var ownerProto = this.getOwnerModel().prototype;
73374 if (oldGetterName) {
73375 delete ownerProto[oldGetterName];
73376 }
73377 if (getterName) {
73378 ownerProto[getterName] = this.createGetter();
73379 }
73380 },
73381
73382 updateSetterName: function(setterName, oldSetterName) {
73383 var ownerProto = this.getOwnerModel().prototype;
73384 if (oldSetterName) {
73385 delete ownerProto[oldSetterName];
73386 }
73387 if (setterName) {
73388 ownerProto[setterName] = this.createSetter();
73389 }
73390 },
73391
73392 /**
73393 * @private
73394 * Returns a setter function to be placed on the owner model's prototype
73395 * @return {Function} The setter function
73396 */
73397 createSetter: function() {
73398 var me = this,
73399 foreignKey = me.getForeignKey(),
73400 instanceName = me.getInstanceName(),
73401 associatedModel = me.getAssociatedModel();
73402
73403 //'this' refers to the Model instance inside this function
73404 return function(value, options, scope) {
73405 var Model = Ext.data.Model,
73406 record;
73407
73408 if (value && value.isModel) {
73409 value = value.getId();
73410 }
73411
73412 this.set(foreignKey, value);
73413
73414 if (value || value === 0) {
73415 record = Model.cache[Model.generateCacheId(associatedModel.modelName, value)];
73416 if (record) {
73417 this[instanceName] = record;
73418 }
73419 } else {
73420 delete this[instanceName];
73421 }
73422
73423 if (Ext.isFunction(options)) {
73424 options = {
73425 callback: options,
73426 scope: scope || this
73427 };
73428 }
73429
73430 if (Ext.isObject(options)) {
73431 return this.save(options);
73432 }
73433
73434 return this;
73435 };
73436 },
73437
73438 /**
73439 * @private
73440 * Returns a getter function to be placed on the owner model's prototype. We cache the loaded instance
73441 * the first time it is loaded so that subsequent calls to the getter always receive the same reference.
73442 * @return {Function} The getter function
73443 */
73444 createGetter: function() {
73445 var me = this,
73446 associatedModel = me.getAssociatedModel(),
73447 foreignKey = me.getForeignKey(),
73448 instanceName = me.getInstanceName();
73449
73450 //'this' refers to the Model instance inside this function
73451 return function(options, scope) {
73452 options = options || {};
73453
73454 var model = this,
73455 foreignKeyId = model.get(foreignKey),
73456 success, instance, args;
73457
73458 if (options.reload === true || model[instanceName] === undefined) {
73459 if (typeof options == 'function') {
73460 options = {
73461 callback: options,
73462 scope: scope || model
73463 };
73464 }
73465
73466 // Overwrite the success handler so we can assign the current instance
73467 success = options.success;
73468 options.success = function(rec){
73469 model[instanceName] = rec;
73470 if (success) {
73471 success.apply(this, arguments);
73472 }
73473 };
73474
73475 associatedModel.load(foreignKeyId, options);
73476 } else {
73477 instance = model[instanceName];
73478 args = [instance];
73479 scope = scope || model;
73480
73481 Ext.callback(options, scope, args);
73482 Ext.callback(options.success, scope, args);
73483 Ext.callback(options.failure, scope, args);
73484 Ext.callback(options.callback, scope, args);
73485
73486 return instance;
73487 }
73488 };
73489 },
73490
73491 /**
73492 * Read associated data
73493 * @private
73494 * @param {Ext.data.Model} record The record we're writing to
73495 * @param {Ext.data.reader.Reader} reader The reader for the associated model
73496 * @param {Object} associationData The raw associated data
73497 */
73498 read: function(record, reader, associationData) {
73499 var inverse = this.getInverseAssociation(),
73500 newRecord = reader.read([associationData]).getRecords()[0];
73501
73502 record[this.getSetterName()].call(record, newRecord);
73503
73504 //if the inverse association was found, set it now on each record we've just created
73505 if (inverse) {
73506 newRecord[inverse.getInstanceName()] = record;
73507 }
73508 },
73509
73510 getInverseAssociation: function() {
73511 var ownerName = this.getOwnerModel().modelName;
73512
73513 return this.getAssociatedModel().associations.findBy(function(assoc) {
73514 return assoc.getType().toLowerCase() === 'belongsto' && assoc.getAssociatedModel().modelName === ownerName;
73515 });
73516 }
73517 });
73518
73519 /**
73520 * @author Ed Spencer
73521 * @class Ext.data.Error
73522 *
73523 * This is used when validating a record. The validate method will return an {@link Ext.data.Errors} collection
73524 * containing Ext.data.Error instances. Each error has a field and a message.
73525 *
73526 * Usually this class does not need to be instantiated directly - instances are instead created
73527 * automatically when {@link Ext.data.Model#validate validate} on a model instance.
73528 */
73529
73530 Ext.define('Ext.data.Error', {
73531 config: {
73532 /**
73533 * @cfg {String} field
73534 * The name of the field this error belongs to.
73535 */
73536 field: null,
73537
73538 /**
73539 * @cfg {String} message
73540 * The message containing the description of the error.
73541 */
73542 message: ''
73543 },
73544
73545 constructor: function(config) {
73546 this.initConfig(config);
73547 }
73548 });
73549
73550 /**
73551 * @author Ed Spencer
73552 * @class Ext.data.Errors
73553 * @extends Ext.util.Collection
73554 *
73555 * Wraps a collection of validation error responses and provides convenient functions for
73556 * accessing and errors for specific fields.
73557 *
73558 * Usually this class does not need to be instantiated directly - instances are instead created
73559 * automatically when {@link Ext.data.Model#validate validate} on a model instance:
73560 *
73561 * //validate some existing model instance - in this case it returned two failures messages
73562 * var errors = myModel.validate();
73563 *
73564 * errors.isValid(); // false
73565 *
73566 * errors.length; // 2
73567 * errors.getByField('name'); // [{field: 'name', message: 'must be present'}]
73568 * errors.getByField('title'); // [{field: 'title', message: 'is too short'}]
73569 */
73570 Ext.define('Ext.data.Errors', {
73571 extend: Ext.util.Collection ,
73572
73573
73574
73575 /**
73576 * Returns `true` if there are no errors in the collection.
73577 * @return {Boolean}
73578 */
73579 isValid: function() {
73580 return this.length === 0;
73581 },
73582
73583 /**
73584 * Returns all of the errors for the given field.
73585 * @param {String} fieldName The field to get errors for.
73586 * @return {Object[]} All errors for the given field.
73587 */
73588 getByField: function(fieldName) {
73589 var errors = [],
73590 error, i;
73591
73592 for (i = 0; i < this.length; i++) {
73593 error = this.items[i];
73594
73595 if (error.getField() == fieldName) {
73596 errors.push(error);
73597 }
73598 }
73599
73600 return errors;
73601 },
73602
73603 add: function() {
73604 var obj = arguments.length == 1 ? arguments[0] : arguments[1];
73605
73606 if (!(obj instanceof Ext.data.Error)) {
73607 obj = Ext.create('Ext.data.Error', {
73608 field: obj.field || obj.name,
73609 message: obj.error || obj.message
73610 });
73611 }
73612
73613 return this.callParent([obj]);
73614 }
73615 });
73616
73617 /**
73618 * @author Ed Spencer
73619 *
73620 * This singleton contains a set of validation functions that can be used to validate any type of data. They are most
73621 * often used in {@link Ext.data.Model Models}, where they are automatically set up and executed.
73622 *
73623 * ###Further Reading
73624 * [Sencha Touch Data Overview](../../../core_concepts/data/data_package_overview.html)
73625 * [Sencha Touch Store Guide](../../../core_concepts/data/stores.html)
73626 * [Sencha Touch Models Guide](../../../core_concepts/data/models.html)
73627 * [Sencha Touch Proxy Guide](../../../core_concepts/data/proxies.html)
73628 */
73629 Ext.define('Ext.data.Validations', {
73630 alternateClassName: 'Ext.data.validations',
73631
73632 singleton: true,
73633
73634 config: {
73635 /**
73636 * @property {String} presenceMessage
73637 * The default error message used when a presence validation fails.
73638 */
73639 presenceMessage: 'must be present',
73640
73641 /**
73642 * @property {String} lengthMessage
73643 * The default error message used when a length validation fails.
73644 */
73645 lengthMessage: 'is the wrong length',
73646
73647 /**
73648 * @property {String} formatMessage
73649 * The default error message used when a format validation fails.
73650 */
73651 formatMessage: 'is the wrong format',
73652
73653 /**
73654 * @property {String} inclusionMessage
73655 * The default error message used when an inclusion validation fails.
73656 */
73657 inclusionMessage: 'is not included in the list of acceptable values',
73658
73659 /**
73660 * @property {String} exclusionMessage
73661 * The default error message used when an exclusion validation fails.
73662 */
73663 exclusionMessage: 'is not an acceptable value',
73664
73665 /**
73666 * @property {String} emailMessage
73667 * The default error message used when an email validation fails
73668 */
73669 emailMessage: 'is not a valid email address'
73670 },
73671
73672 constructor: function(config) {
73673 this.initConfig(config);
73674 },
73675
73676 /**
73677 * Returns the configured error message for any of the validation types.
73678 * @param {String} type The type of validation you want to get the error message for.
73679 * @return {Object}
73680 */
73681 getMessage: function(type) {
73682 var getterFn = this['get' + type[0].toUpperCase() + type.slice(1) + 'Message'];
73683 if (getterFn) {
73684 return getterFn.call(this);
73685 }
73686 return '';
73687 },
73688
73689 /**
73690 * The regular expression used to validate email addresses
73691 * @property emailRe
73692 * @type RegExp
73693 */
73694 emailRe: /^\s*[\w\-\+_]+(\.[\w\-\+_]+)*\@[\w\-\+_]+\.[\w\-\+_]+(\.[\w\-\+_]+)*\s*$/,
73695
73696 /**
73697 * Validates that the given value is present.
73698 * For example:
73699 *
73700 * validations: [{type: 'presence', field: 'age'}]
73701 *
73702 * @param {Object} config Config object.
73703 * @param {Object} value The value to validate.
73704 * @return {Boolean} `true` if validation passed.
73705 */
73706 presence: function(config, value) {
73707 if (arguments.length === 1) {
73708 value = config;
73709 }
73710 return !!value || value === 0;
73711 },
73712
73713 /**
73714 * Returns `true` if the given value is between the configured min and max values.
73715 * For example:
73716 *
73717 * validations: [{type: 'length', field: 'name', min: 2}]
73718 *
73719 * @param {Object} config Config object.
73720 * @param {String} value The value to validate.
73721 * @return {Boolean} `true` if the value passes validation.
73722 */
73723 length: function(config, value) {
73724 if (value === undefined || value === null) {
73725 return false;
73726 }
73727
73728 var length = value.length,
73729 min = config.min,
73730 max = config.max;
73731
73732 if ((min && length < min) || (max && length > max)) {
73733 return false;
73734 } else {
73735 return true;
73736 }
73737 },
73738
73739 /**
73740 * Validates that an email string is in the correct format.
73741 * @param {Object} config Config object.
73742 * @param {String} email The email address.
73743 * @return {Boolean} `true` if the value passes validation.
73744 */
73745 email: function(config, email) {
73746 return Ext.data.validations.emailRe.test(email);
73747 },
73748
73749 /**
73750 * Returns `true` if the given value passes validation against the configured `matcher` regex.
73751 * For example:
73752 *
73753 * validations: [{type: 'format', field: 'username', matcher: /([a-z]+)[0-9]{2,3}/}]
73754 *
73755 * @param {Object} config Config object.
73756 * @param {String} value The value to validate.
73757 * @return {Boolean} `true` if the value passes the format validation.
73758 */
73759 format: function(config, value) {
73760 if (value === undefined || value === null) {
73761 value = '';
73762 }
73763 return !!(config.matcher && config.matcher.test(value));
73764 },
73765
73766 /**
73767 * Validates that the given value is present in the configured `list`.
73768 * For example:
73769 *
73770 * validations: [{type: 'inclusion', field: 'gender', list: ['Male', 'Female']}]
73771 *
73772 * @param {Object} config Config object.
73773 * @param {String} value The value to validate.
73774 * @return {Boolean} `true` if the value is present in the list.
73775 */
73776 inclusion: function(config, value) {
73777 return config.list && Ext.Array.indexOf(config.list,value) != -1;
73778 },
73779
73780 /**
73781 * Validates that the given value is not present in the configured `list`.
73782 *
73783 * For example:
73784 *
73785 * validations: [{type: 'exclusion', field: 'username', list: ['Admin', 'Operator']}]
73786 *
73787 * @param {Object} config Config object.
73788 * @param {String} value The value to validate.
73789 * @return {Boolean} `true` if the value is not present in the list.
73790 */
73791 exclusion: function(config, value) {
73792 return config.list && Ext.Array.indexOf(config.list,value) == -1;
73793 }
73794 });
73795
73796 /**
73797 * @author Ed Spencer
73798 *
73799 * A Model represents some object that your application manages. For example, one might define a Model for Users,
73800 * Products, Cars, or any other real-world object that we want to model in the system. Models are registered via the
73801 * {@link Ext.data.ModelManager model manager}, and are used by {@link Ext.data.Store stores}, which are in turn used by many
73802 * of the data-bound components in Ext.
73803 *
73804 * Models are defined as a set of fields and any arbitrary methods and properties relevant to the model. For example:
73805 *
73806 * Ext.define('User', {
73807 * extend: 'Ext.data.Model',
73808 *
73809 * config: {
73810 * fields: [
73811 * {name: 'name', type: 'string'},
73812 * {name: 'age', type: 'int'},
73813 * {name: 'phone', type: 'string'},
73814 * {name: 'alive', type: 'boolean', defaultValue: true}
73815 * ]
73816 * },
73817 *
73818 * changeName: function() {
73819 * var oldName = this.get('name'),
73820 * newName = oldName + " The Barbarian";
73821 *
73822 * this.set('name', newName);
73823 * }
73824 * });
73825 *
73826 * The fields array is turned into a {@link Ext.util.MixedCollection MixedCollection} automatically by the {@link
73827 * Ext.data.ModelManager ModelManager}, and all other functions and properties are copied to the new Model's prototype.
73828 *
73829 * Now we can create instances of our User model and call any model logic we defined:
73830 *
73831 * var user = Ext.create('User', {
73832 * name : 'Conan',
73833 * age : 24,
73834 * phone: '555-555-5555'
73835 * });
73836 *
73837 * user.changeName();
73838 * user.get('name'); // returns "Conan The Barbarian"
73839 *
73840 * # Validations
73841 *
73842 * Models have built-in support for validations, which are executed against the validator functions in {@link
73843 * Ext.data.validations} ({@link Ext.data.validations see all validation functions}). Validations are easy to add to
73844 * models:
73845 *
73846 * Ext.define('User', {
73847 * extend: 'Ext.data.Model',
73848 *
73849 * config: {
73850 * fields: [
73851 * {name: 'name', type: 'string'},
73852 * {name: 'age', type: 'int'},
73853 * {name: 'phone', type: 'string'},
73854 * {name: 'gender', type: 'string'},
73855 * {name: 'username', type: 'string'},
73856 * {name: 'alive', type: 'boolean', defaultValue: true}
73857 * ],
73858 *
73859 * validations: [
73860 * {type: 'presence', field: 'age'},
73861 * {type: 'length', field: 'name', min: 2},
73862 * {type: 'inclusion', field: 'gender', list: ['Male', 'Female']},
73863 * {type: 'exclusion', field: 'username', list: ['Admin', 'Operator']},
73864 * {type: 'format', field: 'username', matcher: /([a-z]+)[0-9]{2,3}/}
73865 * ]
73866 * }
73867 * });
73868 *
73869 * The validations can be run by simply calling the {@link #validate} function, which returns a {@link Ext.data.Errors}
73870 * object:
73871 *
73872 * var instance = Ext.create('User', {
73873 * name: 'Ed',
73874 * gender: 'Male',
73875 * username: 'edspencer'
73876 * });
73877 *
73878 * var errors = instance.validate();
73879 *
73880 * # Associations
73881 *
73882 * Models can have associations with other Models via {@link Ext.data.association.HasOne},
73883 * {@link Ext.data.association.BelongsTo belongsTo} and {@link Ext.data.association.HasMany hasMany} associations.
73884 * For example, let's say we're writing a blog administration application which deals with Users, Posts and Comments.
73885 * We can express the relationships between these models like this:
73886 *
73887 * Ext.define('Post', {
73888 * extend: 'Ext.data.Model',
73889 *
73890 * config: {
73891 * fields: ['id', 'user_id'],
73892 * belongsTo: 'User',
73893 * hasMany : {model: 'Comment', name: 'comments'}
73894 * }
73895 * });
73896 *
73897 * Ext.define('Comment', {
73898 * extend: 'Ext.data.Model',
73899 *
73900 * config: {
73901 * fields: ['id', 'user_id', 'post_id'],
73902 * belongsTo: 'Post'
73903 * }
73904 * });
73905 *
73906 * Ext.define('User', {
73907 * extend: 'Ext.data.Model',
73908 *
73909 * config: {
73910 * fields: ['id'],
73911 * hasMany: [
73912 * 'Post',
73913 * {model: 'Comment', name: 'comments'}
73914 * ]
73915 * }
73916 * });
73917 *
73918 * See the docs for {@link Ext.data.association.HasOne}, {@link Ext.data.association.BelongsTo} and
73919 * {@link Ext.data.association.HasMany} for details on the usage and configuration of associations.
73920 * Note that associations can also be specified like this:
73921 *
73922 * Ext.define('User', {
73923 * extend: 'Ext.data.Model',
73924 *
73925 * config: {
73926 * fields: ['id'],
73927 * associations: [
73928 * {type: 'hasMany', model: 'Post', name: 'posts'},
73929 * {type: 'hasMany', model: 'Comment', name: 'comments'}
73930 * ]
73931 * }
73932 * });
73933 *
73934 * # Using a Proxy
73935 *
73936 * Models are great for representing types of data and relationships, but sooner or later we're going to want to load or
73937 * save that data somewhere. All loading and saving of data is handled via a {@link Ext.data.proxy.Proxy Proxy}, which
73938 * can be set directly on the Model:
73939 *
73940 * Ext.define('User', {
73941 * extend: 'Ext.data.Model',
73942 *
73943 * config: {
73944 * fields: ['id', 'name', 'email'],
73945 * proxy: {
73946 * type: 'rest',
73947 * url : '/users'
73948 * }
73949 * }
73950 * });
73951 *
73952 * 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
73953 * RESTful backend. Let's see how this works:
73954 *
73955 * var user = Ext.create('User', {name: 'Ed Spencer', email: 'ed@sencha.com'});
73956 *
73957 * user.save(); //POST /users
73958 *
73959 * Calling {@link #save} on the new Model instance tells the configured RestProxy that we wish to persist this Model's
73960 * data onto our server. RestProxy figures out that this Model hasn't been saved before because it doesn't have an id,
73961 * and performs the appropriate action - in this case issuing a POST request to the url we configured (/users). We
73962 * configure any Proxy on any Model and always follow this API - see {@link Ext.data.proxy.Proxy} for a full list.
73963 *
73964 * Loading data via the Proxy is equally easy:
73965 *
73966 * //get a reference to the User model class
73967 * var User = Ext.ModelManager.getModel('User');
73968 *
73969 * //Uses the configured RestProxy to make a GET request to /users/123
73970 * User.load(123, {
73971 * success: function(user) {
73972 * console.log(user.getId()); //logs 123
73973 * }
73974 * });
73975 *
73976 * Models can also be updated and destroyed easily:
73977 *
73978 * //the user Model we loaded in the last snippet:
73979 * user.set('name', 'Edward Spencer');
73980 *
73981 * //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
73982 * user.save({
73983 * success: function() {
73984 * console.log('The User was updated');
73985 * }
73986 * });
73987 *
73988 * //tells the Proxy to destroy the Model. Performs a DELETE request to /users/123
73989 * user.erase({
73990 * success: function() {
73991 * console.log('The User was destroyed!');
73992 * }
73993 * });
73994 *
73995 * # Usage in Stores
73996 *
73997 * 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
73998 * creating a {@link Ext.data.Store Store}:
73999 *
74000 * var store = Ext.create('Ext.data.Store', {
74001 * model: 'User'
74002 * });
74003 *
74004 * //uses the Proxy we set up on Model to load the Store data
74005 * store.load();
74006 *
74007 * A Store is just a collection of Model instances - usually loaded from a server somewhere. Store can also maintain a
74008 * set of added, updated and removed Model instances to be synchronized with the server via the Proxy. See the {@link
74009 * Ext.data.Store Store docs} for more information on Stores.
74010 *
74011 * ###Further Reading
74012 * [Sencha Touch Data Overview](../../../core_concepts/data/data_package_overview.html)
74013 * [Sencha Touch Store Guide](../../../core_concepts/data/stores.html)
74014 * [Sencha Touch Models Guide](../../../core_concepts/data/models.html)
74015 * [Sencha Touch Proxy Guide](../../../core_concepts/data/proxies.html)
74016 */
74017 Ext.define('Ext.data.Model', {
74018 alternateClassName: 'Ext.data.Record',
74019
74020 mixins: {
74021 observable: Ext.mixin.Observable
74022 },
74023
74024 /**
74025 * Provides an easy way to quickly determine if a given class is a Model
74026 * @property isModel
74027 * @type Boolean
74028 * @private
74029 */
74030 isModel: true,
74031
74032
74033
74034
74035
74036
74037
74038
74039
74040
74041
74042
74043
74044 config: {
74045 /**
74046 * @cfg {String} idProperty
74047 * The name of the field treated as this Model's unique `id`. Note that this field
74048 * needs to have a type of 'auto'. Setting the field type to anything else will be undone by the
74049 * framework. This is because new records that are created without an `id`, will have one generated.
74050 */
74051 idProperty: 'id',
74052
74053 data: null,
74054
74055 /**
74056 * @cfg {Object[]/String[]} fields
74057 * The {@link Ext.data.Model field} definitions for all instances of this Model.
74058 *
74059 * __Note:__ this does not set the *values* of each
74060 * field on an instance, it sets the collection of fields itself.
74061 *
74062 * Sample usage:
74063 *
74064 * Ext.define('MyApp.model.User', {
74065 * extend: 'Ext.data.Model',
74066 *
74067 * config: {
74068 * fields: [
74069 * 'id',
74070 * {name: 'age', type: 'int'},
74071 * {name: 'taxRate', type: 'float'}
74072 * ]
74073 * }
74074 * });
74075 * @accessor
74076 */
74077 fields: undefined,
74078
74079 /**
74080 * @cfg {Object[]} validations
74081 * An array of {@link Ext.data.Validations validations} for this model.
74082 */
74083 validations: null,
74084
74085 /**
74086 * @cfg {Object[]} associations
74087 * An array of {@link Ext.data.association.Association associations} for this model.
74088 */
74089 associations: null,
74090
74091 /**
74092 * @cfg {String/Object/String[]/Object[]} hasMany
74093 * One or more {@link Ext.data.association.HasMany HasMany associations} for this model.
74094 */
74095 hasMany: null,
74096
74097 /**
74098 * @cfg {String/Object/String[]/Object[]} hasOne
74099 * One or more {@link Ext.data.association.HasOne HasOne associations} for this model.
74100 */
74101 hasOne: null,
74102
74103 /**
74104 * @cfg {String/Object/String[]/Object[]} belongsTo
74105 * One or more {@link Ext.data.association.BelongsTo BelongsTo associations} for this model.
74106 */
74107 belongsTo: null,
74108
74109 /**
74110 * @cfg {Object/Ext.data.Proxy} proxy
74111 * The string type of the default Model Proxy.
74112 * @accessor
74113 */
74114 proxy: null,
74115
74116
74117 /**
74118 * @cfg {Object/String} identifier
74119 * The identifier strategy used when creating new instances of this Model that don't have an id defined.
74120 * By default this uses the simple identifier strategy that generates id's like 'ext-record-12'. If you are
74121 * saving these records in localstorage using a LocalStorage proxy you need to ensure that this identifier
74122 * strategy is set to something that always generates unique id's. We provide one strategy by default that
74123 * generates these unique id's which is the {@link Ext.data.identifier.Uuid} strategy.
74124 */
74125 identifier: {
74126 type: 'simple'
74127 },
74128
74129 /**
74130 * @cfg {String} clientIdProperty
74131 * The name of a property that is used for submitting this Model's unique client-side identifier
74132 * to the server when multiple phantom records are saved as part of the same {@link Ext.data.Operation Operation}.
74133 * In such a case, the server response should include the client id for each record
74134 * so that the server response data can be used to update the client-side records if necessary.
74135 * This property cannot have the same name as any of this Model's fields.
74136 * @accessor
74137 */
74138 clientIdProperty: 'clientId',
74139
74140 /**
74141 * @method getIsErased Returns `true` if the record has been erased on the server.
74142 */
74143 isErased: false,
74144
74145 /**
74146 * @cfg {Boolean} useCache
74147 * Change this to `false` if you want to ensure that new instances are created for each id. For example,
74148 * this is needed when adding the same tree nodes to multiple trees.
74149 */
74150 useCache: true
74151 },
74152
74153 staticConfigs: [
74154 'idProperty',
74155 'fields',
74156 'validations',
74157 'associations',
74158 'hasMany',
74159 'hasOne',
74160 'belongsTo',
74161 'clientIdProperty',
74162 'identifier',
74163 'useCache',
74164 'proxy'
74165 ],
74166
74167 statics: {
74168 EDIT : 'edit',
74169 REJECT : 'reject',
74170 COMMIT : 'commit',
74171
74172 cache: {},
74173
74174 generateProxyMethod: function(name) {
74175 return function() {
74176 var prototype = this.prototype;
74177 return prototype[name].apply(prototype, arguments);
74178 };
74179 },
74180
74181 generateCacheId: function(record, id) {
74182 var modelName;
74183
74184 if (record && record.isModel) {
74185 modelName = record.modelName;
74186 if (id === undefined) {
74187 id = record.getId();
74188 }
74189 } else {
74190 modelName = record;
74191 }
74192
74193 return modelName.replace(/\./g, '-').toLowerCase() + '-' + id;
74194 }
74195 },
74196
74197 inheritableStatics: {
74198 /**
74199 * Asynchronously loads a model instance by id. Sample usage:
74200 *
74201 * MyApp.User = Ext.define('User', {
74202 * extend: 'Ext.data.Model',
74203 * fields: [
74204 * {name: 'id', type: 'int'},
74205 * {name: 'name', type: 'string'}
74206 * ]
74207 * });
74208 *
74209 * MyApp.User.load(10, {
74210 * scope: this,
74211 * failure: function(record, operation) {
74212 * //do something if the load failed
74213 * },
74214 * success: function(record, operation) {
74215 * //do something if the load succeeded
74216 * },
74217 * callback: function(record, operation) {
74218 * //do something whether the load succeeded or failed
74219 * }
74220 * });
74221 *
74222 * @param {Number} id The id of the model to load
74223 * @param {Object} [config] Config object containing fields:
74224 * @param {Function} config.success Called on success.
74225 * @param {Function} config.failure Called on failure.
74226 * @param {Function} config.callback Called after load.
74227 * @param {Object} config.scope Value of `this` in the above functions.
74228 * @param {Object} [scope] Same as `config.scope`.
74229 * @static
74230 * @inheritable
74231 */
74232 load: function(id, config, scope) {
74233 var proxy = this.getProxy(),
74234 idProperty = this.getIdProperty(),
74235 record = null,
74236 params = {},
74237 callback, operation;
74238
74239 scope = scope || (config && config.scope) || this;
74240 if (Ext.isFunction(config)) {
74241 config = {
74242 callback: config,
74243 scope: scope
74244 };
74245 }
74246
74247 params[idProperty] = id;
74248 config = Ext.apply({}, config);
74249 config = Ext.applyIf(config, {
74250 action: 'read',
74251 params: params,
74252 model: this
74253 });
74254
74255 operation = Ext.create('Ext.data.Operation', config);
74256
74257 if (!proxy) {
74258 Ext.Logger.error('You are trying to load a model that doesn\'t have a Proxy specified');
74259 }
74260
74261 callback = function(operation) {
74262 if (operation.wasSuccessful()) {
74263 record = operation.getRecords()[0] || null;
74264 Ext.callback(config.success, scope, [record, operation]);
74265 } else {
74266 Ext.callback(config.failure, scope, [record, operation]);
74267 }
74268 Ext.callback(config.callback, scope, [record, operation]);
74269 };
74270
74271 proxy.read(operation, callback, this);
74272 }
74273 },
74274
74275 /**
74276 * @property {Boolean} editing
74277 * @readonly
74278 * Internal flag used to track whether or not the model instance is currently being edited.
74279 */
74280 editing : false,
74281
74282 /**
74283 * @property {Boolean} dirty
74284 * @readonly
74285 * `true` if this Record has been modified.
74286 */
74287 dirty : false,
74288
74289 /**
74290 * @property {Boolean} phantom
74291 * `true` when the record does not yet exist in a server-side database (see {@link #setDirty}).
74292 * Any record which has a real database pk set as its id property is NOT a phantom -- it's real.
74293 */
74294 phantom : false,
74295
74296 /**
74297 * Creates new Model instance.
74298 * @param {Object} data An object containing keys corresponding to this model's fields, and their associated values.
74299 * @param {Number} id (optional) Unique ID to assign to this model instance.
74300 * @param {Object} [raw]
74301 * @param {Object} [convertedData]
74302 */
74303 constructor: function(data, id, raw, convertedData) {
74304 var me = this,
74305 cached = null,
74306 useCache = me.getUseCache(),
74307 idProperty = me.getIdProperty();
74308
74309 data = data || convertedData || {};
74310
74311 // We begin by checking if an id is passed to the constructor. If this is the case we override
74312 // any possible id value that was passed in the data.
74313 if (id || id === 0) {
74314 // Lets skip using set here since it's so much faster
74315 data[idProperty] = me.internalId = id;
74316 }
74317
74318 // If we have an id set in the data we check to see if we already have a cached instance
74319 // of this record. If that's the case then we just return that instance with the updated data
74320 // passed to this constructor.
74321 id = data[idProperty];
74322 if (useCache && (id || id === 0)) {
74323 cached = Ext.data.Model.cache[Ext.data.Model.generateCacheId(this, id)];
74324 if (cached) {
74325 cached.raw = raw || cached.raw;
74326 return cached.mergeData(convertedData || data || {});
74327 }
74328 }
74329
74330 /**
74331 * @property {Object} modified key/value pairs of all fields whose values have changed.
74332 * The value is the original value for the field.
74333 */
74334 me.modified = {};
74335
74336 /**
74337 * @property {Object} raw The raw data used to create this model if created via a reader.
74338 */
74339 me.raw = raw || data || {};
74340
74341 /**
74342 * @property {Array} stores
74343 * An array of {@link Ext.data.Store} objects that this record is bound to.
74344 */
74345 me.stores = [];
74346
74347 if (convertedData) {
74348 me.setConvertedData(data);
74349 } else {
74350 me.setData(data);
74351 }
74352
74353 // me.id is always set to be randomly generated and should only be used for binding Observable events!
74354 me.id = me.getIdentifier().generate(me);
74355
74356 // If it does not have an id in the data at this point, we use the one generated by the id strategy.
74357 // This means that we will treat this record as a phantom record from now on
74358 id = me.data[idProperty];
74359 if (!id && id !== 0) {
74360 me.data[idProperty] = me.internalId = me.id;
74361 me.phantom = true;
74362
74363 // If we only now set an id on this model, it means we setData won't have handled the inline
74364 // association data, which we will have to do now then.
74365 if (this.associations.length) {
74366 this.handleInlineAssociationData(data);
74367 }
74368 }
74369 else {
74370 // We also want to make sure that the internalId has the same value as the id in
74371 // the data object.
74372 this.internalId = id;
74373 }
74374
74375 if (useCache) {
74376 Ext.data.Model.cache[Ext.data.Model.generateCacheId(me)] = me;
74377 }
74378
74379 if (this.init && typeof this.init == 'function') {
74380 this.init();
74381 }
74382 },
74383
74384 /**
74385 * Private function that is used when you create a record that already exists in the model cache.
74386 * In this case we loop over each field, and apply any data to the current instance that is not already
74387 * marked as being dirty on that instance.
74388 * @param {Object} data
74389 * @return {Ext.data.Model} This record.
74390 * @private
74391 */
74392 mergeData: function(rawData) {
74393 var me = this,
74394 fields = me.getFields().items,
74395 ln = fields.length,
74396 modified = me.modified,
74397 modifiedFieldNames = [],
74398 data = me.data,
74399 i, field, fieldName, value, id;
74400
74401 for (i = 0; i < ln; i++) {
74402 field = fields[i];
74403 fieldName = field._name;
74404 value = rawData[fieldName];
74405
74406 if (value !== undefined && !modified.hasOwnProperty(fieldName)) {
74407 if (field._convert) {
74408 value = field._convert(value, me);
74409 }
74410
74411 if(data[fieldName] !== value) {
74412 if(modifiedFieldNames.length === 0 && !me.editing) {
74413 this.beginEdit()
74414 }
74415
74416 modifiedFieldNames.push(fieldName);
74417 }
74418
74419 data[fieldName] = value;
74420 } else if (Ext.isFunction(field._convert)) {
74421 value = field._convert(value, me);
74422 data[fieldName] = value;
74423 }
74424 }
74425
74426 if (me.associations.length) {
74427 me.handleInlineAssociationData(rawData);
74428 }
74429
74430 if(modifiedFieldNames.length > 0 && me.editing) {
74431 this.endEdit(false, modifiedFieldNames);
74432 }
74433
74434 return this;
74435 },
74436
74437 /**
74438 * This method is used to set the data for this Record instance.
74439 * Note that the existing data is removed. If a field is not specified
74440 * in the passed data it will use the field's default value. If a convert
74441 * method is specified for the field it will be called on the value.
74442 * @param {Object} rawData
74443 * @return {Ext.data.Model} This record.
74444 */
74445 setData: function(rawData) {
74446 var me = this,
74447 fields = me.fields.items,
74448 ln = fields.length,
74449 isArray = Ext.isArray(rawData),
74450 data = me._data = me.data = {},
74451 i, field, name, value, convert, id;
74452
74453 if (!rawData) {
74454 return me;
74455 }
74456
74457 for (i = 0; i < ln; i++) {
74458 field = fields[i];
74459 name = field._name;
74460 convert = field._convert;
74461
74462 if (isArray) {
74463 value = rawData[i];
74464 }
74465 else {
74466 value = rawData[name];
74467 if (typeof value == 'undefined') {
74468 value = field._defaultValue;
74469 }
74470 }
74471
74472 if (convert) {
74473 value = field._convert(value, me);
74474 }
74475
74476 data[name] = value;
74477 }
74478
74479 id = me.getId();
74480 if (me.associations.length && (id || id === 0)) {
74481 me.handleInlineAssociationData(rawData);
74482 }
74483
74484 return me;
74485 },
74486
74487 handleInlineAssociationData: function(data) {
74488 var associations = this.associations.items,
74489 ln = associations.length,
74490 i, association, associationData, reader, proxy, associationKey;
74491
74492 data = Ext.apply({}, data, this.raw);
74493
74494 for (i = 0; i < ln; i++) {
74495 association = associations[i];
74496 associationKey = association.getAssociationKey();
74497 associationData = data[associationKey];
74498
74499 if (associationData) {
74500 reader = association.getReader();
74501 if (!reader) {
74502 proxy = association.getAssociatedModel().getProxy();
74503 // if the associated model has a Reader already, use that, otherwise attempt to create a sensible one
74504 if (proxy) {
74505 reader = proxy.getReader();
74506 } else {
74507 reader = new Ext.data.JsonReader({
74508 model: association.getAssociatedModel()
74509 });
74510 }
74511 }
74512 association.read(this, reader, associationData);
74513 }
74514 }
74515 },
74516
74517 /**
74518 * Sets the model instance's id field to the given id.
74519 * @param {Number/String} id The new id
74520 */
74521 setId: function(id) {
74522 var currentId = this.getId();
74523
74524 // Lets use the direct property instead of getter here
74525 this.set(this.getIdProperty(), id);
74526
74527 // We don't update the this.id since we don't want to break listeners that already
74528 // exist on the record instance.
74529 this.internalId = id;
74530
74531 if (this.getUseCache()) {
74532 delete Ext.data.Model.cache[Ext.data.Model.generateCacheId(this, currentId)];
74533 Ext.data.Model.cache[Ext.data.Model.generateCacheId(this)] = this;
74534 }
74535 },
74536
74537 /**
74538 * Returns the unique ID allocated to this model instance as defined by {@link #idProperty}.
74539 * @return {Number/String} The `id`.
74540 */
74541 getId: function() {
74542 // Lets use the direct property instead of getter here
74543 return this.get(this.getIdProperty());
74544 },
74545
74546 /**
74547 * This sets the data directly without converting and applying default values.
74548 * This method is used when a Record gets instantiated by a Reader. Only use
74549 * this when you are sure you are passing correctly converted data.
74550 * @param {Object} data
74551 * @return {Ext.data.Model} This Record.
74552 */
74553 setConvertedData: function(data) {
74554 this._data = this.data = data;
74555 return this;
74556 },
74557
74558 /**
74559 * Returns the value of the given field.
74560 * @param {String} fieldName The field to fetch the value for.
74561 * @return {Object} The value.
74562 */
74563 get: function(fieldName) {
74564 return this.data[fieldName];
74565 },
74566
74567 /**
74568 * Sets the given field to the given value, marks the instance as dirty.
74569 * @param {String/Object} fieldName The field to set, or an object containing key/value pairs.
74570 * @param {Object} value The value to set.
74571 */
74572 set: function(fieldName, value) {
74573 var me = this,
74574 // We are using the fields map since it saves lots of function calls
74575 fieldMap = me.fields.map,
74576 modified = me.modified,
74577 notEditing = !me.editing,
74578 modifiedCount = 0,
74579 modifiedFieldNames = [],
74580 field, key, i, currentValue, ln, convert;
74581
74582 /*
74583 * If we're passed an object, iterate over that object. NOTE: we pull out fields with a convert function and
74584 * set those last so that all other possible data is set before the convert function is called
74585 */
74586 if (arguments.length == 1) {
74587 for (key in fieldName) {
74588 if (fieldName.hasOwnProperty(key)) {
74589 //here we check for the custom convert function. Note that if a field doesn't have a convert function,
74590 //we default it to its type's convert function, so we have to check that here. This feels rather dirty.
74591 field = fieldMap[key];
74592 if (field && field.hasCustomConvert()) {
74593 modifiedFieldNames.push(key);
74594 continue;
74595 }
74596
74597 if (!modifiedCount && notEditing) {
74598 me.beginEdit();
74599 }
74600 ++modifiedCount;
74601 me.set(key, fieldName[key]);
74602 }
74603 }
74604
74605 ln = modifiedFieldNames.length;
74606 if (ln) {
74607 if (!modifiedCount && notEditing) {
74608 me.beginEdit();
74609 }
74610 modifiedCount += ln;
74611 for (i = 0; i < ln; i++) {
74612 field = modifiedFieldNames[i];
74613 me.set(field, fieldName[field]);
74614 }
74615 }
74616
74617 if (notEditing && modifiedCount) {
74618 me.endEdit(false, modifiedFieldNames);
74619 }
74620 } else if(modified) {
74621 field = fieldMap[fieldName];
74622 convert = field && field.getConvert();
74623 if (convert) {
74624 value = convert.call(field, value, me);
74625 }
74626
74627 currentValue = me.data[fieldName];
74628 me.data[fieldName] = value;
74629
74630 if (field && !me.isEqual(currentValue, value)) {
74631 if (modified.hasOwnProperty(fieldName)) {
74632 if (me.isEqual(modified[fieldName], value)) {
74633 // the original value in me.modified equals the new value, so the
74634 // field is no longer modified
74635 delete modified[fieldName];
74636 // we might have removed the last modified field, so check to see if
74637 // there are any modified fields remaining and correct me.dirty:
74638 me.dirty = false;
74639 for (key in modified) {
74640 if (modified.hasOwnProperty(key)) {
74641 me.dirty = true;
74642 break;
74643 }
74644 }
74645 }
74646 } else {
74647 me.dirty = true;
74648 // We only go one level back?
74649 modified[fieldName] = currentValue;
74650 }
74651 }
74652
74653 if (notEditing) {
74654 me.afterEdit([fieldName], modified);
74655 }
74656 }
74657 },
74658
74659 /**
74660 * Checks if two values are equal, taking into account certain
74661 * special factors, for example dates.
74662 * @private
74663 * @param {Object} a The first value.
74664 * @param {Object} b The second value.
74665 * @return {Boolean} `true` if the values are equal.
74666 */
74667 isEqual: function(a, b){
74668 if (Ext.isDate(a) && Ext.isDate(b)) {
74669 return a.getTime() === b.getTime();
74670 }
74671 return a === b;
74672 },
74673
74674 /**
74675 * Begins an edit. While in edit mode, no events (e.g. the `update` event) are relayed to the containing store.
74676 * When an edit has begun, it must be followed by either {@link #endEdit} or {@link #cancelEdit}.
74677 */
74678 beginEdit: function() {
74679 var me = this;
74680 if (!me.editing) {
74681 me.editing = true;
74682
74683 // We save the current states of dirty, data and modified so that when we
74684 // cancel the edit, we can put it back to this state
74685 me.dirtySave = me.dirty;
74686 me.dataSave = Ext.apply({}, me.data);
74687 me.modifiedSave = Ext.apply({}, me.modified);
74688 }
74689 },
74690
74691 /**
74692 * Cancels all changes made in the current edit operation.
74693 */
74694 cancelEdit: function() {
74695 var me = this;
74696 if (me.editing) {
74697 me.editing = false;
74698
74699 // Reset the modified state, nothing changed since the edit began
74700 me.modified = me.modifiedSave;
74701 me.data = me.dataSave;
74702 me.dirty = me.dirtySave;
74703
74704 // Delete the saved states
74705 delete me.modifiedSave;
74706 delete me.dataSave;
74707 delete me.dirtySave;
74708 }
74709 },
74710
74711 /**
74712 * Ends an edit. If any data was modified, the containing store is notified (ie, the store's `update` event will
74713 * fire).
74714 * @param {Boolean} silent `true` to not notify the store of the change.
74715 * @param {String[]} modifiedFieldNames Array of field names changed during edit.
74716 */
74717 endEdit: function(silent, modifiedFieldNames) {
74718 var me = this;
74719
74720 if (me.editing) {
74721 me.editing = false;
74722
74723 if (silent !== true && (me.changedWhileEditing())) {
74724 me.afterEdit(modifiedFieldNames || Ext.Object.getKeys(this.modified), this.modified);
74725 }
74726
74727 delete me.modifiedSave;
74728 delete me.dataSave;
74729 delete me.dirtySave;
74730 }
74731 },
74732
74733 /**
74734 * Checks if the underlying data has changed during an edit. This doesn't necessarily
74735 * mean the record is dirty, however we still need to notify the store since it may need
74736 * to update any views.
74737 * @private
74738 * @return {Boolean} `true` if the underlying data has changed during an edit.
74739 */
74740 changedWhileEditing: function() {
74741 var me = this,
74742 saved = me.dataSave,
74743 data = me.data,
74744 key;
74745
74746 for (key in data) {
74747 if (data.hasOwnProperty(key)) {
74748 if (!me.isEqual(data[key], saved[key])) {
74749 return true;
74750 }
74751 }
74752 }
74753
74754 return false;
74755 },
74756
74757 /**
74758 * Gets a hash of only the fields that have been modified since this Model was created or committed.
74759 * @return {Object}
74760 */
74761 getChanges : function() {
74762 var modified = this.modified,
74763 changes = {},
74764 field;
74765
74766 for (field in modified) {
74767 if (modified.hasOwnProperty(field)) {
74768 changes[field] = this.get(field);
74769 }
74770 }
74771
74772 return changes;
74773 },
74774
74775 /**
74776 * Returns `true` if the passed field name has been `{@link #modified}` since the load or last commit.
74777 * @param {String} fieldName {@link Ext.data.Field#name}
74778 * @return {Boolean}
74779 */
74780 isModified : function(fieldName) {
74781 return this.modified.hasOwnProperty(fieldName);
74782 },
74783
74784 /**
74785 * Saves the model instance using the configured proxy.
74786 *
74787 * @param {Object/Function} [options] Options to pass to the proxy. Config object for {@link
74788 * Ext.data.Operation}. If you pass a function, this will automatically become the callback
74789 * method. For convenience the config object may also contain `success` and `failure` methods in
74790 * addition to `callback` - they will all be invoked with the Model and Operation as arguments.
74791 *
74792 * @param {Object} [scope] The scope to run your callback method in. This is only used if you
74793 * passed a function as the first argument.
74794 *
74795 * @return {Ext.data.Model} The Model instance
74796 */
74797 save: function(options, scope) {
74798 var me = this,
74799 action = me.phantom ? 'create' : 'update',
74800 proxy = me.getProxy(),
74801 operation,
74802 callback;
74803
74804 if (!proxy) {
74805 Ext.Logger.error('You are trying to save a model instance that doesn\'t have a Proxy specified');
74806 }
74807
74808 options = options || {};
74809 scope = scope || me;
74810
74811 if (Ext.isFunction(options)) {
74812 options = {
74813 callback: options,
74814 scope: scope
74815 };
74816 }
74817
74818 Ext.applyIf(options, {
74819 records: [me],
74820 action : action,
74821 model: me.self
74822 });
74823
74824 operation = Ext.create('Ext.data.Operation', options);
74825
74826 callback = function(operation) {
74827 if (operation.wasSuccessful()) {
74828 Ext.callback(options.success, scope, [me, operation]);
74829 } else {
74830 Ext.callback(options.failure, scope, [me, operation]);
74831 }
74832
74833 Ext.callback(options.callback, scope, [me, operation]);
74834 };
74835
74836 proxy[action](operation, callback, me);
74837
74838 return me;
74839 },
74840
74841 /**
74842 * Destroys the record using the configured proxy. This will create a 'destroy' operation.
74843 * Note that this doesn't destroy this instance after the server comes back with a response.
74844 * It will however call `afterErase` on any Stores it is joined to. Stores by default will
74845 * automatically remove this instance from their data collection.
74846 *
74847 * @param {Object/Function} options Options to pass to the proxy. Config object for {@link Ext.data.Operation}.
74848 * If you pass a function, this will automatically become the callback method. For convenience the config
74849 * object may also contain `success` and `failure` methods in addition to `callback` - they will all be invoked
74850 * with the Model and Operation as arguments.
74851 * @param {Object} scope The scope to run your callback method in. This is only used if you passed a function
74852 * as the first argument.
74853 * @return {Ext.data.Model} The Model instance.
74854 */
74855 erase: function(options, scope) {
74856 var me = this,
74857 proxy = this.getProxy(),
74858 operation,
74859 callback;
74860
74861 if (!proxy) {
74862 Ext.Logger.error('You are trying to erase a model instance that doesn\'t have a Proxy specified');
74863 }
74864
74865 options = options || {};
74866 scope = scope || me;
74867
74868 if (Ext.isFunction(options)) {
74869 options = {
74870 callback: options,
74871 scope: scope
74872 };
74873 }
74874
74875 Ext.applyIf(options, {
74876 records: [me],
74877 action : 'destroy',
74878 model: this.self
74879 });
74880
74881 operation = Ext.create('Ext.data.Operation', options);
74882
74883 callback = function(operation) {
74884 if (operation.wasSuccessful()) {
74885 Ext.callback(options.success, scope, [me, operation]);
74886 } else {
74887 Ext.callback(options.failure, scope, [me, operation]);
74888 }
74889
74890 Ext.callback(options.callback, scope, [me, operation]);
74891 };
74892
74893 proxy.destroy(operation, callback, me);
74894
74895 return me;
74896 },
74897
74898 /**
74899 * Usually called by the {@link Ext.data.Store} to which this model instance has been {@link #join joined}. Rejects
74900 * all changes made to the model instance since either creation, or the last commit operation. Modified fields are
74901 * reverted to their original values.
74902 *
74903 * Developers should subscribe to the {@link Ext.data.Store#update} event to have their code notified of reject
74904 * operations.
74905 *
74906 * @param {Boolean} [silent=false] (optional) `true` to skip notification of the owning store of the change.
74907 */
74908 reject: function(silent) {
74909 var me = this,
74910 modified = me.modified,
74911 field;
74912
74913 for (field in modified) {
74914 if (modified.hasOwnProperty(field)) {
74915 if (typeof modified[field] != "function") {
74916 me.data[field] = modified[field];
74917 }
74918 }
74919 }
74920
74921 me.dirty = false;
74922 me.editing = false;
74923 me.modified = {};
74924
74925 if (silent !== true) {
74926 me.afterReject();
74927 }
74928 },
74929
74930 /**
74931 * Usually called by the {@link Ext.data.Store} which owns the model instance. Commits all changes made to the
74932 * instance since either creation or the last commit operation.
74933 *
74934 * Developers should subscribe to the {@link Ext.data.Store#update} event to have their code notified of commit
74935 * operations.
74936 *
74937 * @param {Boolean} [silent=false] (optional) `true` to skip notification of the owning store of the change.
74938 */
74939 commit: function(silent) {
74940 var me = this,
74941 modified = this.modified;
74942
74943 me.phantom = me.dirty = me.editing = false;
74944 me.modified = {};
74945
74946 if (silent !== true) {
74947 me.afterCommit(modified);
74948 }
74949 },
74950
74951 /**
74952 * @private
74953 * If this Model instance has been {@link #join joined} to a {@link Ext.data.Store store}, the store's
74954 * `afterEdit` method is called.
74955 * @param {String[]} modifiedFieldNames Array of field names changed during edit.
74956 * @param {Object} modified
74957 */
74958 afterEdit : function(modifiedFieldNames, modified) {
74959 this.notifyStores('afterEdit', modifiedFieldNames, modified);
74960 },
74961
74962 /**
74963 * @private
74964 * If this Model instance has been {@link #join joined} to a {@link Ext.data.Store store}, the store's
74965 * `afterReject` method is called.
74966 */
74967 afterReject : function() {
74968 this.notifyStores("afterReject");
74969 },
74970
74971 /**
74972 * @private
74973 * If this Model instance has been {@link #join joined} to a {@link Ext.data.Store store}, the store's
74974 * `afterCommit` method is called.
74975 */
74976 afterCommit: function(modified) {
74977 this.notifyStores('afterCommit', Ext.Object.getKeys(modified || {}), modified);
74978 },
74979
74980 /**
74981 * @private
74982 * Helper function used by {@link #afterEdit}, {@link #afterReject}, and {@link #afterCommit}. Calls the given method on the
74983 * {@link Ext.data.Store store} that this instance has {@link #join join}ed, if any. The store function
74984 * will always be called with the model instance as its single argument.
74985 * @param {String} fn The function to call on the store.
74986 */
74987 notifyStores: function(fn) {
74988 var args = Ext.Array.clone(arguments),
74989 stores = this.stores;
74990 if (Ext.isArray(stores)) {
74991 var ln = stores.length,
74992 i, store;
74993
74994 args[0] = this;
74995 for (i = 0; i < ln; ++i) {
74996 store = stores[i];
74997 if (store !== undefined && typeof store[fn] == "function") {
74998 store[fn].apply(store, args);
74999 }
75000 }
75001 }
75002 },
75003
75004 /**
75005 * Creates a copy (clone) of this Model instance.
75006 *
75007 * @param {String} id A new `id`. If you don't specify this a new `id` will be generated for you.
75008 * To generate a phantom instance with a new `id` use:
75009 *
75010 * var rec = record.copy(); // clone the record with a new id
75011 *
75012 * @return {Ext.data.Model}
75013 */
75014 copy: function(newId) {
75015 var me = this,
75016 idProperty = me.getIdProperty(),
75017 raw = Ext.apply({}, me.raw),
75018 data = Ext.apply({}, me.data);
75019
75020 delete raw[idProperty];
75021 delete data[idProperty];
75022
75023 return new me.self(null, newId, raw, data);
75024 },
75025
75026 /**
75027 * Returns an object containing the data set on this record. This method also allows you to
75028 * retrieve all the associated data. Note that if you should always use this method if you
75029 * need all the associated data, since the data property on the record instance is not
75030 * ensured to be updated at all times.
75031 * @param {Boolean} includeAssociated `true` to include the associated data.
75032 * @return {Object} The data.
75033 */
75034 getData: function(includeAssociated) {
75035 var data = this.data;
75036
75037 if (includeAssociated === true) {
75038 Ext.apply(data, this.getAssociatedData());
75039 }
75040
75041 return data;
75042 },
75043
75044 /**
75045 * Gets all of the data from this Models *loaded* associations. It does this recursively - for example if we have a
75046 * User which `hasMany` Orders, and each Order `hasMany` OrderItems, it will return an object like this:
75047 *
75048 * {
75049 * orders: [
75050 * {
75051 * id: 123,
75052 * status: 'shipped',
75053 * orderItems: [
75054 * // ...
75055 * ]
75056 * }
75057 * ]
75058 * }
75059 *
75060 * @return {Object} The nested data set for the Model's loaded associations.
75061 */
75062 getAssociatedData: function() {
75063 return this.prepareAssociatedData(this, [], null);
75064 },
75065
75066 /**
75067 * @private
75068 * This complex-looking method takes a given Model instance and returns an object containing all data from
75069 * all of that Model's *loaded* associations. See {@link #getAssociatedData}
75070 * @param {Ext.data.Model} record The Model instance
75071 * @param {String[]} ids PRIVATE. The set of Model instance `internalIds` that have already been loaded
75072 * @param {String} associationType (optional) The name of the type of association to limit to.
75073 * @return {Object} The nested data set for the Model's loaded associations.
75074 */
75075 prepareAssociatedData: function(record, ids, associationType) {
75076 //we keep track of all of the internalIds of the models that we have loaded so far in here
75077 var associations = record.associations.items,
75078 associationCount = associations.length,
75079 associationData = {},
75080 recursiveAssociationQueue = [],
75081 associatedStore, associationName, associatedRecords, associatedRecord,
75082 associatedRecordCount, association, id, i, j, type, allow, recursiveAssociationItem;
75083
75084 for (i = 0; i < associationCount; i++) {
75085 association = associations[i];
75086 associationName = association.getName();
75087 type = association.getType();
75088 allow = true;
75089
75090 if (associationType) {
75091 allow = type == associationType;
75092 }
75093
75094 if (allow && type.toLowerCase() == 'hasmany') {
75095 //this is the hasMany store filled with the associated data
75096 associatedStore = record[association.getStoreName()];
75097
75098 //we will use this to contain each associated record's data
75099 associationData[associationName] = [];
75100
75101 //if it's loaded, put it into the association data
75102 if (associatedStore && associatedStore.getCount() > 0) {
75103 associatedRecords = associatedStore.data.items;
75104 associatedRecordCount = associatedRecords.length;
75105
75106 recursiveAssociationQueue.length = 0;
75107 //now we're finally iterating over the records in the association. We do this recursively
75108 for (j = 0; j < associatedRecordCount; j++) {
75109 associatedRecord = associatedRecords[j];
75110 // Use the id, since it is prefixed with the model name, guaranteed to be unique
75111 id = associatedRecord.id;
75112
75113 //when we load the associations for a specific model instance we add it to the set of loaded ids so that
75114 //we don't load it twice. If we don't do this, we can fall into endless recursive loading failures.
75115 if (Ext.Array.indexOf(ids, id) == -1) {
75116 ids.push(id);
75117
75118 associationData[associationName][j] = associatedRecord.getData();
75119 recursiveAssociationQueue.push({
75120 associationName:associationName,
75121 j:j,
75122 associatedRecord:associatedRecord,
75123 ids:ids,
75124 associationType:associationType
75125 });
75126 }
75127 }
75128
75129 while (recursiveAssociationQueue.length > 0) {
75130 recursiveAssociationItem = recursiveAssociationQueue.shift();
75131 Ext.apply(associationData[recursiveAssociationItem.associationName][recursiveAssociationItem.j], this.prepareAssociatedData(recursiveAssociationItem.associatedRecord, recursiveAssociationItem.ids, recursiveAssociationItem.associationType));
75132 }
75133 }
75134 } else if (allow && (type.toLowerCase() == 'belongsto' || type.toLowerCase() == 'hasone')) {
75135 associatedRecord = record[association.getInstanceName()];
75136 if (associatedRecord !== undefined) {
75137 id = associatedRecord.id;
75138 if (Ext.Array.indexOf(ids, id) === -1) {
75139 ids.push(id);
75140 associationData[associationName] = associatedRecord.getData();
75141 Ext.apply(associationData[associationName], this.prepareAssociatedData(associatedRecord, ids, associationType));
75142 }
75143 }
75144 }
75145 }
75146
75147 return associationData;
75148 },
75149
75150 /**
75151 * By joining this model to an instance of a class, this model will automatically try to
75152 * call certain template methods on that instance ({@link #afterEdit}, {@link #afterCommit}, {@link Ext.data.Store#afterErase}).
75153 * For example, a Store calls join and unjoin whenever you add or remove a record to it's data collection.
75154 * This way a Store can get notified of any changes made to this record.
75155 * This functionality is usually only required when creating custom components.
75156 * @param {Ext.data.Store} store The store to which this model has been added.
75157 */
75158 join: function(store) {
75159 Ext.Array.include(this.stores, store);
75160 },
75161
75162 /**
75163 * This un-joins this record from an instance of a class. Look at the documentation for {@link #join}
75164 * for more information about joining records to class instances.
75165 * @param {Ext.data.Store} store The store from which this model has been removed.
75166 */
75167 unjoin: function(store) {
75168 Ext.Array.remove(this.stores, store);
75169 },
75170
75171 /**
75172 * Marks this **Record** as `{@link #dirty}`. This method is used internally when adding `{@link #phantom}` records
75173 * to a {@link Ext.data.proxy.Server#writer writer enabled store}.
75174 *
75175 * Marking a record `{@link #dirty}` causes the phantom to be returned by {@link Ext.data.Store#getUpdatedRecords}
75176 * where it will have a create action composed for it during {@link Ext.data.Model#save model save} operations.
75177 */
75178 setDirty : function() {
75179 var me = this,
75180 name;
75181
75182 me.dirty = true;
75183
75184 me.fields.each(function(field) {
75185 if (field.getPersist()) {
75186 name = field.getName();
75187 me.modified[name] = me.get(name);
75188 }
75189 });
75190 },
75191
75192 /**
75193 * Validates the current data against all of its configured {@link #cfg-validations}.
75194 * @return {Ext.data.Errors} The errors object.
75195 */
75196 validate: function() {
75197 var errors = Ext.create('Ext.data.Errors'),
75198 validations = this.getValidations().items,
75199 validators = Ext.data.Validations,
75200 length, validation, field, valid, type, i;
75201
75202 if (validations) {
75203 length = validations.length;
75204
75205 for (i = 0; i < length; i++) {
75206 validation = validations[i];
75207 field = validation.field || validation.name;
75208 type = validation.type;
75209 valid = validators[type](validation, this.get(field));
75210
75211 if (!valid) {
75212 errors.add(Ext.create('Ext.data.Error', {
75213 field : field,
75214 message: validation.message || validators.getMessage(type)
75215 }));
75216 }
75217 }
75218 }
75219
75220 return errors;
75221 },
75222
75223 /**
75224 * Checks if the model is valid. See {@link #validate}.
75225 * @return {Boolean} `true` if the model is valid.
75226 */
75227 isValid: function(){
75228 return this.validate().isValid();
75229 },
75230
75231 /**
75232 * Returns a url-suitable string for this model instance. By default this just returns the name of the Model class
75233 * followed by the instance ID - for example an instance of MyApp.model.User with ID 123 will return 'user/123'.
75234 * @return {String} The url string for this model instance.
75235 */
75236 toUrl: function() {
75237 var pieces = this.$className.split('.'),
75238 name = pieces[pieces.length - 1].toLowerCase();
75239
75240 return name + '/' + this.getId();
75241 },
75242
75243 /**
75244 * Destroys this model instance. Note that this doesn't do a 'destroy' operation. If you want to destroy
75245 * the record in your localStorage or on the server you should use the {@link #erase} method.
75246 */
75247 destroy: function() {
75248 var me = this;
75249 me.notifyStores('afterErase', me);
75250 if (me.getUseCache()) {
75251 delete Ext.data.Model.cache[Ext.data.Model.generateCacheId(me)];
75252 }
75253 me.raw = me.stores = me.modified = null;
75254 me.callParent(arguments);
75255 },
75256
75257 //<debug>
75258 markDirty : function() {
75259 if (Ext.isDefined(Ext.Logger)) {
75260 Ext.Logger.deprecate('Ext.data.Model: markDirty has been deprecated. Use setDirty instead.');
75261 }
75262 return this.setDirty.apply(this, arguments);
75263 },
75264 //</debug>
75265
75266 applyProxy: function(proxy, currentProxy) {
75267 return Ext.factory(proxy, Ext.data.Proxy, currentProxy, 'proxy');
75268 },
75269
75270 updateProxy: function(proxy) {
75271 if (proxy) {
75272 proxy.setModel(this.self);
75273 }
75274 },
75275
75276 applyAssociations: function(associations) {
75277 if (associations) {
75278 this.addAssociations(associations, 'hasMany');
75279 }
75280 },
75281
75282 applyBelongsTo: function(belongsTo) {
75283 if (belongsTo) {
75284 this.addAssociations(belongsTo, 'belongsTo');
75285 }
75286 },
75287
75288 applyHasMany: function(hasMany) {
75289 if (hasMany) {
75290 this.addAssociations(hasMany, 'hasMany');
75291 }
75292 },
75293
75294 applyHasOne: function(hasOne) {
75295 if (hasOne) {
75296 this.addAssociations(hasOne, 'hasOne');
75297 }
75298 },
75299
75300 addAssociations: function(associations, defaultType) {
75301 var ln, i, association,
75302 name = this.self.modelName,
75303 associationsCollection = this.self.associations,
75304 onCreatedFn;
75305
75306 associations = Ext.Array.from(associations);
75307
75308 for (i = 0, ln = associations.length; i < ln; i++) {
75309 association = associations[i];
75310 if (!Ext.isObject(association)) {
75311 association = {model: association};
75312 }
75313
75314 Ext.applyIf(association, {
75315 type: defaultType,
75316 ownerModel: name,
75317 associatedModel: association.model
75318 });
75319
75320 delete association.model;
75321
75322 onCreatedFn = Ext.Function.bind(function(associationName) {
75323 associationsCollection.add(Ext.data.association.Association.create(this));
75324 }, association);
75325
75326 Ext.ClassManager.onCreated(onCreatedFn, this, (typeof association.associatedModel === 'string') ? association.associatedModel : Ext.getClassName(association.associatedModel));
75327 }
75328 },
75329
75330 applyValidations: function(validations) {
75331 if (validations) {
75332 if (!Ext.isArray(validations)) {
75333 validations = [validations];
75334 }
75335 this.addValidations(validations);
75336 }
75337 },
75338
75339 addValidations: function(validations) {
75340 this.self.validations.addAll(validations);
75341 },
75342
75343 /**
75344 * @method setFields
75345 * Updates the collection of Fields that all instances of this Model use. **Does not** update field values in a Model
75346 * instance (use {@link #set} for that), instead this updates which fields are available on the Model class. This
75347 * is normally used when creating or updating Model definitions dynamically, for example if you allow your users to
75348 * define their own Models and save the fields configuration to a database, this method allows you to change those
75349 * fields later.
75350 * @return {Array}
75351 */
75352
75353 applyFields: function(fields) {
75354 var superFields = this.superclass.fields;
75355 if (superFields) {
75356 fields = superFields.items.concat(fields || []);
75357 }
75358 return fields || [];
75359 },
75360
75361 updateFields: function(fields) {
75362 var ln = fields.length,
75363 me = this,
75364 prototype = me.self.prototype,
75365 idProperty = this.getIdProperty(),
75366 idField, fieldsCollection, field, i;
75367
75368 /**
75369 * @property {Ext.util.MixedCollection} fields
75370 * The fields defined on this model.
75371 */
75372 fieldsCollection = me._fields = me.fields = new Ext.util.Collection(prototype.getFieldName);
75373
75374 for (i = 0; i < ln; i++) {
75375 field = fields[i];
75376 if (!field.isField) {
75377 field = new Ext.data.Field(fields[i]);
75378 }
75379 fieldsCollection.add(field);
75380 }
75381
75382 // We want every Model to have an id property field
75383 idField = fieldsCollection.get(idProperty);
75384 if (!idField) {
75385 fieldsCollection.add(new Ext.data.Field(idProperty));
75386 } else {
75387 idField.setType('auto');
75388 }
75389
75390 fieldsCollection.addSorter(prototype.sortConvertFields);
75391 },
75392
75393 applyIdentifier: function(identifier) {
75394 if (typeof identifier === 'string') {
75395 identifier = {
75396 type: identifier
75397 };
75398 }
75399 return Ext.factory(identifier, Ext.data.identifier.Simple, this.getIdentifier(), 'data.identifier');
75400 },
75401
75402 /**
75403 * This method is used by the fields collection to retrieve the key for a field
75404 * based on it's name.
75405 * @param {String} field
75406 * @return {String}
75407 * @private
75408 */
75409 getFieldName: function(field) {
75410 return field.getName();
75411 },
75412
75413 /**
75414 * This method is being used to sort the fields based on their convert method. If
75415 * a field has a custom convert method, we ensure its more to the bottom of the collection.
75416 * @param {String} field1
75417 * @param {String} field2
75418 * @return {Number}
75419 * @private
75420 */
75421 sortConvertFields: function(field1, field2) {
75422 var f1SpecialConvert = field1.hasCustomConvert(),
75423 f2SpecialConvert = field2.hasCustomConvert();
75424
75425 if (f1SpecialConvert && !f2SpecialConvert) {
75426 return 1;
75427 }
75428 if (!f1SpecialConvert && f2SpecialConvert) {
75429 return -1;
75430 }
75431 return 0;
75432 },
75433
75434 /**
75435 * @private
75436 */
75437 onClassExtended: function(cls, data, hooks) {
75438 var onBeforeClassCreated = hooks.onBeforeCreated,
75439 Model = this,
75440 prototype = Model.prototype,
75441 configNameCache = Ext.Class.configNameCache,
75442 staticConfigs = prototype.staticConfigs.concat(data.staticConfigs || []),
75443 defaultConfig = prototype.config,
75444 config = data.config || {},
75445 key;
75446
75447 // Convert old properties in data into a config object
75448 data.config = config;
75449
75450 hooks.onBeforeCreated = function(cls, data) {
75451 var dependencies = [],
75452 prototype = cls.prototype,
75453 statics = {},
75454 config = prototype.config,
75455 staticConfigsLn = staticConfigs.length,
75456 copyMethods = ['set', 'get'],
75457 copyMethodsLn = copyMethods.length,
75458 associations = config.associations || [],
75459 name = Ext.getClassName(cls),
75460 key, methodName, i, j, ln;
75461
75462 // Create static setters and getters for each config option
75463 for (i = 0; i < staticConfigsLn; i++) {
75464 key = staticConfigs[i];
75465
75466 for (j = 0; j < copyMethodsLn; j++) {
75467 methodName = configNameCache[key][copyMethods[j]];
75468 if (methodName in prototype) {
75469 statics[methodName] = Model.generateProxyMethod(methodName);
75470 }
75471 }
75472 }
75473
75474 cls.addStatics(statics);
75475
75476 // Save modelName on class and its prototype
75477 cls.modelName = name;
75478 prototype.modelName = name;
75479
75480 // Take out dependencies on other associations and the proxy type
75481 if (config.belongsTo) {
75482 dependencies.push('association.belongsto');
75483 }
75484 if (config.hasMany) {
75485 dependencies.push('association.hasmany');
75486 }
75487 if (config.hasOne) {
75488 dependencies.push('association.hasone');
75489 }
75490
75491 for (i = 0,ln = associations.length; i < ln; ++i) {
75492 dependencies.push('association.' + associations[i].type.toLowerCase());
75493 }
75494
75495 if (config.identifier) {
75496 if (typeof config.identifier === 'string') {
75497 dependencies.push('data.identifier.' + config.identifier);
75498 }
75499 else if (typeof config.identifier.type === 'string') {
75500 dependencies.push('data.identifier.' + config.identifier.type);
75501 }
75502 }
75503
75504 if (config.proxy) {
75505 if (typeof config.proxy === 'string') {
75506 dependencies.push('proxy.' + config.proxy);
75507 }
75508 else if (typeof config.proxy.type === 'string') {
75509 dependencies.push('proxy.' + config.proxy.type);
75510 }
75511 }
75512
75513 if (config.validations) {
75514 dependencies.push('Ext.data.Validations');
75515 }
75516
75517 Ext.require(dependencies, function() {
75518 Ext.Function.interceptBefore(hooks, 'onCreated', function() {
75519 Ext.data.ModelManager.registerType(name, cls);
75520
75521 var superCls = cls.prototype.superclass;
75522
75523 /**
75524 * @property {Ext.util.Collection} associations
75525 * The associations defined on this model.
75526 */
75527 cls.prototype.associations = cls.associations = cls.prototype._associations = (superCls && superCls.associations)
75528 ? superCls.associations.clone()
75529 : new Ext.util.Collection(function(association) {
75530 return association.getName();
75531 });
75532
75533 /**
75534 * @property {Ext.util.Collection} validations
75535 * The validations defined on this model.
75536 */
75537 cls.prototype.validations = cls.validations = cls.prototype._validations = (superCls && superCls.validations)
75538 ? superCls.validations.clone()
75539 : new Ext.util.Collection(function(validation) {
75540 return validation.field ? (validation.field + '-' + validation.type) : (validation.name + '-' + validation.type);
75541 });
75542
75543 cls.prototype = Ext.Object.chain(cls.prototype);
75544 cls.prototype.initConfig.call(cls.prototype, config);
75545
75546 delete cls.prototype.initConfig;
75547 });
75548
75549 onBeforeClassCreated.call(Model, cls, data, hooks);
75550 });
75551 };
75552 }
75553 });
75554
75555 /**
75556 * @private
75557 */
75558 Ext.define('Ext.util.Grouper', {
75559
75560 /* Begin Definitions */
75561
75562 extend: Ext.util.Sorter ,
75563
75564 isGrouper: true,
75565
75566 config: {
75567 /**
75568 * @cfg {Function} groupFn This function will be called for each item in the collection to
75569 * determine the group to which it belongs.
75570 * @cfg {Object} groupFn.item The current item from the collection
75571 * @cfg {String} groupFn.return The group identifier for the item
75572 */
75573 groupFn: null,
75574
75575 /**
75576 * @cfg {String} sortProperty You can define this configuration if you want the groups to be sorted
75577 * on something other then the group string returned by the `groupFn`.
75578 */
75579 sortProperty: null,
75580
75581 /**
75582 * @cfg {Function} sorterFn
75583 * Grouper has a custom sorterFn that cannot be overridden by the user. If a property has been defined
75584 * on this grouper, we use the default `sorterFn`, else we sort based on the returned group string.
75585 */
75586 sorterFn: function(item1, item2) {
75587 var property = this.getSortProperty(),
75588 groupFn, group1, group2, modifier;
75589
75590 groupFn = this.getGroupFn();
75591 group1 = groupFn.call(this, item1);
75592 group2 = groupFn.call(this, item2);
75593
75594 if (property) {
75595 if (group1 !== group2) {
75596 return this.defaultSortFn.call(this, item1, item2);
75597 } else {
75598 return 0;
75599 }
75600 }
75601 return (group1 > group2) ? 1 : ((group1 < group2) ? -1 : 0);
75602 }
75603 },
75604
75605 /**
75606 * @private
75607 * Basic default sorter function that just compares the defined property of each object.
75608 */
75609 defaultSortFn: function(item1, item2) {
75610 var me = this,
75611 transform = me._transform,
75612 root = me._root,
75613 value1, value2,
75614 property = me._sortProperty;
75615
75616 if (root !== null) {
75617 item1 = item1[root];
75618 item2 = item2[root];
75619 }
75620
75621 value1 = item1[property];
75622 value2 = item2[property];
75623
75624 if (transform) {
75625 value1 = transform(value1);
75626 value2 = transform(value2);
75627 }
75628
75629 return value1 > value2 ? 1 : (value1 < value2 ? -1 : 0);
75630 },
75631
75632 updateProperty: function(property) {
75633 this.setGroupFn(this.standardGroupFn);
75634 },
75635
75636 standardGroupFn: function(item) {
75637 var root = this.getRoot(),
75638 property = this.getProperty(),
75639 data = item;
75640
75641 if (root) {
75642 data = item[root];
75643 }
75644
75645 return data[property];
75646 },
75647
75648 getGroupString: function(item) {
75649 var group = this.getGroupFn().call(this, item);
75650 return (group !== null && typeof group != 'undefined') ? group.toString() : '';
75651 }
75652 });
75653
75654 /**
75655 * @author Ed Spencer
75656 *
75657 * The Store class encapsulates a client side cache of {@link Ext.data.Model Model} objects. Stores load
75658 * data via a {@link Ext.data.proxy.Proxy Proxy}, and also provide functions for {@link #sort sorting},
75659 * {@link #filter filtering} and querying the {@link Ext.data.Model model} instances contained within it.
75660 *
75661 * Creating a Store is easy - we just tell it the Model and the Proxy to use to load and save its data:
75662 *
75663 * // Set up a {@link Ext.data.Model model} to use in our Store
75664 * Ext.define("User", {
75665 * extend: "Ext.data.Model",
75666 * config: {
75667 * fields: [
75668 * {name: "firstName", type: "string"},
75669 * {name: "lastName", type: "string"},
75670 * {name: "age", type: "int"},
75671 * {name: "eyeColor", type: "string"}
75672 * ]
75673 * }
75674 * });
75675 *
75676 * var myStore = Ext.create("Ext.data.Store", {
75677 * model: "User",
75678 * proxy: {
75679 * type: "ajax",
75680 * url : "/users.json",
75681 * reader: {
75682 * type: "json",
75683 * rootProperty: "users"
75684 * }
75685 * },
75686 * autoLoad: true
75687 * });
75688 *
75689 * Ext.create("Ext.List", {
75690 * fullscreen: true,
75691 * store: myStore,
75692 * itemTpl: "{lastName}, {firstName} ({age})"
75693 * });
75694 *
75695 * In the example above we configured an AJAX proxy to load data from the url '/users.json'. We told our Proxy
75696 * to use a {@link Ext.data.reader.Json JsonReader} to parse the response from the server into Model object -
75697 * {@link Ext.data.reader.Json see the docs on JsonReader} for details.
75698 *
75699 * The external data file, _/users.json_, is as follows:
75700 *
75701 * {
75702 * "success": true,
75703 * "users": [
75704 * {
75705 * "firstName": "Tommy",
75706 * "lastName": "Maintz",
75707 * "age": 24,
75708 * "eyeColor": "green"
75709 * },
75710 * {
75711 * "firstName": "Aaron",
75712 * "lastName": "Conran",
75713 * "age": 26,
75714 * "eyeColor": "blue"
75715 * },
75716 * {
75717 * "firstName": "Jamie",
75718 * "lastName": "Avins",
75719 * "age": 37,
75720 * "eyeColor": "brown"
75721 * }
75722 * ]
75723 * }
75724 *
75725 * ## Inline data
75726 *
75727 * Stores can also load data inline. Internally, Store converts each of the objects we pass in as {@link #cfg-data}
75728 * into Model instances:
75729 *
75730 * @example
75731 * // Set up a model to use in our Store
75732 * Ext.define('User', {
75733 * extend: 'Ext.data.Model',
75734 * config: {
75735 * fields: [
75736 * {name: 'firstName', type: 'string'},
75737 * {name: 'lastName', type: 'string'},
75738 * {name: 'age', type: 'int'},
75739 * {name: 'eyeColor', type: 'string'}
75740 * ]
75741 * }
75742 * });
75743 *
75744 * Ext.create("Ext.data.Store", {
75745 * storeId: "usersStore",
75746 * model: "User",
75747 * data : [
75748 * {firstName: "Ed", lastName: "Spencer"},
75749 * {firstName: "Tommy", lastName: "Maintz"},
75750 * {firstName: "Aaron", lastName: "Conran"},
75751 * {firstName: "Jamie", lastName: "Avins"}
75752 * ]
75753 * });
75754 *
75755 * Ext.create("Ext.List", {
75756 * fullscreen: true,
75757 * store: "usersStore",
75758 * itemTpl: "{lastName}, {firstName}"
75759 * });
75760 *
75761 * Loading inline data using the method above is great if the data is in the correct format already (e.g. it doesn't need
75762 * to be processed by a {@link Ext.data.reader.Reader reader}). If your inline data requires processing to decode the data structure,
75763 * use a {@link Ext.data.proxy.Memory MemoryProxy} instead (see the {@link Ext.data.proxy.Memory MemoryProxy} docs for an example).
75764 *
75765 * Additional data can also be loaded locally using {@link #method-add}.
75766 *
75767 * ## Loading Nested Data
75768 *
75769 * Applications often need to load sets of associated data - for example a CRM system might load a User and her Orders.
75770 * 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
75771 * and allow the Reader to automatically populate the associated models. Below is a brief example, see the {@link Ext.data.reader.Reader} intro
75772 * documentation for a full explanation:
75773 *
75774 * // Set up a model to use in our Store
75775 * Ext.define('User', {
75776 * extend: 'Ext.data.Model',
75777 * config: {
75778 * fields: [
75779 * {name: 'name', type: 'string'},
75780 * {name: 'id', type: 'int'}
75781 * ]
75782 * }
75783 * });
75784 *
75785 * var store = Ext.create('Ext.data.Store', {
75786 * autoLoad: true,
75787 * model: "User",
75788 * proxy: {
75789 * type: 'ajax',
75790 * url : 'users.json',
75791 * reader: {
75792 * type: 'json',
75793 * rootProperty: 'users'
75794 * }
75795 * }
75796 * });
75797 *
75798 * Ext.create("Ext.List", {
75799 * fullscreen: true,
75800 * store: store,
75801 * itemTpl: "{name} (id: {id})"
75802 * });
75803 *
75804 * Which would consume a response like this:
75805 *
75806 * {
75807 * "users": [
75808 * {
75809 * "id": 1,
75810 * "name": "Ed",
75811 * "orders": [
75812 * {
75813 * "id": 10,
75814 * "total": 10.76,
75815 * "status": "invoiced"
75816 * },
75817 * {
75818 * "id": 11,
75819 * "total": 13.45,
75820 * "status": "shipped"
75821 * }
75822 * ]
75823 * },
75824 * {
75825 * "id": 3,
75826 * "name": "Tommy",
75827 * "orders": [
75828 * ]
75829 * },
75830 * {
75831 * "id": 4,
75832 * "name": "Jamie",
75833 * "orders": [
75834 * {
75835 * "id": 12,
75836 * "total": 17.76,
75837 * "status": "shipped"
75838 * }
75839 * ]
75840 * }
75841 * ]
75842 * }
75843 *
75844 * See the {@link Ext.data.reader.Reader} intro docs for a full explanation.
75845 *
75846 * ## Filtering and Sorting
75847 *
75848 * Stores can be sorted and filtered - in both cases either remotely or locally. The {@link #sorters} and {@link #filters} are
75849 * held inside {@link Ext.util.MixedCollection MixedCollection} instances to make them easy to manage. Usually it is sufficient to
75850 * either just specify sorters and filters in the Store configuration or call {@link #sort} or {@link #filter}:
75851 *
75852 * // Set up a model to use in our Store
75853 * Ext.define('User', {
75854 * extend: 'Ext.data.Model',
75855 * config: {
75856 * fields: [
75857 * {name: 'firstName', type: 'string'},
75858 * {name: 'lastName', type: 'string'},
75859 * {name: 'age', type: 'int'}
75860 * ]
75861 * }
75862 * });
75863 *
75864 * var store = Ext.create("Ext.data.Store", {
75865 * autoLoad: true,
75866 * model: "User",
75867 * proxy: {
75868 * type: "ajax",
75869 * url : "users.json",
75870 * reader: {
75871 * type: "json",
75872 * rootProperty: "users"
75873 * }
75874 * },
75875 * sorters: [
75876 * {
75877 * property : "age",
75878 * direction: "DESC"
75879 * },
75880 * {
75881 * property : "firstName",
75882 * direction: "ASC"
75883 * }
75884 * ],
75885 * filters: [
75886 * {
75887 * property: "firstName",
75888 * value: /Jamie/
75889 * }
75890 * ]
75891 * });
75892 *
75893 * Ext.create("Ext.List", {
75894 * fullscreen: true,
75895 * store: store,
75896 * itemTpl: "{lastName}, {firstName} ({age})"
75897 * });
75898 *
75899 * And the data file, _users.json_, is as follows:
75900 *
75901 * {
75902 * "success": true,
75903 * "users": [
75904 * {
75905 * "firstName": "Tommy",
75906 * "lastName": "Maintz",
75907 * "age": 24
75908 * },
75909 * {
75910 * "firstName": "Aaron",
75911 * "lastName": "Conran",
75912 * "age": 26
75913 * },
75914 * {
75915 * "firstName": "Jamie",
75916 * "lastName": "Avins",
75917 * "age": 37
75918 * }
75919 * ]
75920 * }
75921 *
75922 * The new Store will keep the configured sorters and filters in the MixedCollection instances mentioned above. By default, sorting
75923 * and filtering are both performed locally by the Store - see {@link #remoteSort} and {@link #remoteFilter} to allow the server to
75924 * perform these operations instead.
75925 *
75926 * Filtering and sorting after the Store has been instantiated is also easy. Calling {@link #filter} adds another filter to the Store
75927 * and automatically filters the dataset (calling {@link #filter} with no arguments simply re-applies all existing filters). Note that by
75928 * default your sorters are automatically reapplied if using local sorting.
75929 *
75930 * store.filter('eyeColor', 'Brown');
75931 *
75932 * Change the sorting at any time by calling {@link #sort}:
75933 *
75934 * store.sort('height', 'ASC');
75935 *
75936 * Note that all existing sorters will be removed in favor of the new sorter data (if {@link #sort} is called with no arguments,
75937 * the existing sorters are just reapplied instead of being removed). To keep existing sorters and add new ones, just add them
75938 * to the MixedCollection:
75939 *
75940 * store.sorters.add(new Ext.util.Sorter({
75941 * property : 'shoeSize',
75942 * direction: 'ASC'
75943 * }));
75944 *
75945 * store.sort();
75946 *
75947 * ## Registering with StoreManager
75948 *
75949 * Any Store that is instantiated with a {@link #storeId} will automatically be registered with the {@link Ext.data.StoreManager StoreManager}.
75950 * This makes it easy to reuse the same store in multiple views:
75951 *
75952 * // this store can be used several times
75953 * Ext.create('Ext.data.Store', {
75954 * model: 'User',
75955 * storeId: 'usersStore'
75956 * });
75957 *
75958 * Ext.create('Ext.List', {
75959 * store: 'usersStore'
75960 * // other config goes here
75961 * });
75962 *
75963 * Ext.create('Ext.view.View', {
75964 * store: 'usersStore'
75965 * // other config goes here
75966 * });
75967 *
75968 * ## Further Reading
75969 *
75970 * Stores are backed up by an ecosystem of classes that enables their operation. To gain a full understanding of these
75971 * pieces and how they fit together, see:
75972 *
75973 * - {@link Ext.data.proxy.Proxy Proxy} - overview of what Proxies are and how they are used
75974 * - {@link Ext.data.Model Model} - the core class in the data package
75975 * - {@link Ext.data.reader.Reader Reader} - used by any subclass of {@link Ext.data.proxy.Server ServerProxy} to read a response
75976 */
75977 Ext.define('Ext.data.Store', {
75978 alias: 'store.store',
75979
75980 extend: Ext.Evented ,
75981
75982
75983
75984
75985
75986
75987
75988
75989
75990
75991 /**
75992 * @event addrecords
75993 * Fired when one or more new Model instances have been added to this Store. You should listen
75994 * for this event if you have to update a representation of the records in this store in your UI.
75995 * If you need the indices of the records that were added please use the store.indexOf(record) method.
75996 * @param {Ext.data.Store} store The store
75997 * @param {Ext.data.Model[]} records The Model instances that were added
75998 */
75999
76000 /**
76001 * @event removerecords
76002 * Fired when one or more Model instances have been removed from this Store. You should listen
76003 * for this event if you have to update a representation of the records in this store in your UI.
76004 * @param {Ext.data.Store} store The Store object
76005 * @param {Ext.data.Model[]} records The Model instances that was removed
76006 * @param {Number[]} indices The indices of the records that were removed. These indices already
76007 * take into account any potential earlier records that you remove. This means that if you loop
76008 * over the records, you can get its current index in your data representation from this array.
76009 */
76010
76011 /**
76012 * @event updaterecord
76013 * Fires when a Model instance has been updated
76014 * @param {Ext.data.Store} this
76015 * @param {Ext.data.Model} record The Model instance that was updated
76016 * @param {Number} newIndex If the update changed the index of the record (due to sorting for example), then
76017 * this gives you the new index in the store.
76018 * @param {Number} oldIndex If the update changed the index of the record (due to sorting for example), then
76019 * this gives you the old index in the store.
76020 * @param {Array} modifiedFieldNames An array containing the field names that have been modified since the
76021 * record was committed or created
76022 * @param {Object} modifiedValues An object where each key represents a field name that had it's value modified,
76023 * and where the value represents the old value for that field. To get the new value in a listener
76024 * you should use the {@link Ext.data.Model#get get} method.
76025 */
76026
76027 /**
76028 * @event update
76029 * @inheritdoc Ext.data.Store#updaterecord
76030 * @removed 2.0 Listen to #updaterecord instead.
76031 */
76032
76033 /**
76034 * @event refresh
76035 * Fires whenever the records in the Store have changed in a way that your representation of the records
76036 * need to be entirely refreshed.
76037 * @param {Ext.data.Store} this The data store
76038 * @param {Ext.util.Collection} data The data collection containing all the records
76039 */
76040
76041 /**
76042 * @event beforeload
76043 * Fires before a request is made for a new data object. If the beforeload handler returns false the load
76044 * action will be canceled. Note that you should not listen for this event in order to refresh the
76045 * data view. Use the {@link #refresh} event for this instead.
76046 * @param {Ext.data.Store} store This Store
76047 * @param {Ext.data.Operation} operation The Ext.data.Operation object that will be passed to the Proxy to
76048 * load the Store
76049 */
76050
76051 /**
76052 * @event load
76053 * Fires whenever records have been loaded into the store. Note that you should not listen
76054 * for this event in order to refresh the data view. Use the {@link #refresh} event for this instead.
76055 * @param {Ext.data.Store} this
76056 * @param {Ext.data.Model[]} records An array of records
76057 * @param {Boolean} successful `true` if the operation was successful.
76058 * @param {Ext.data.Operation} operation The associated operation.
76059 */
76060
76061 /**
76062 * @event write
76063 * Fires whenever a successful write has been made via the configured {@link #proxy Proxy}
76064 * @param {Ext.data.Store} store This Store
76065 * @param {Ext.data.Operation} operation The {@link Ext.data.Operation Operation} object that was used in
76066 * the write
76067 */
76068
76069 /**
76070 * @event beforesync
76071 * Fired before a call to {@link #sync} is executed. Return `false` from any listener to cancel the sync
76072 * @param {Object} options Hash of all records to be synchronized, broken down into create, update and destroy
76073 */
76074
76075 /**
76076 * @event clear
76077 * Fired after the {@link #removeAll} method is called. Note that you should not listen for this event in order
76078 * to refresh the data view. Use the {@link #refresh} event for this instead.
76079 * @param {Ext.data.Store} this
76080 * @return {Ext.data.Store}
76081 */
76082
76083 statics: {
76084 create: function(store) {
76085 if (!store.isStore) {
76086 if (!store.type) {
76087 store.type = 'store';
76088 }
76089 store = Ext.createByAlias('store.' + store.type, store);
76090 }
76091 return store;
76092 }
76093 },
76094
76095 isStore: true,
76096
76097 config: {
76098 /**
76099 * @cfg {String} storeId
76100 * Unique identifier for this store. If present, this Store will be registered with the {@link Ext.data.StoreManager},
76101 * making it easy to reuse elsewhere.
76102 * @accessor
76103 */
76104 storeId: undefined,
76105
76106 /**
76107 * @cfg {Object[]/Ext.data.Model[]} data
76108 * Array of Model instances or data objects to load locally. See "Inline data" above for details.
76109 * @accessor
76110 */
76111 data: null,
76112
76113 /**
76114 * @cfg {Boolean/Object} [autoLoad=false]
76115 * If data is not specified, and if `autoLoad` is `true` or an Object, this store's load method is automatically called
76116 * after creation. If the value of `autoLoad` is an Object, this Object will be passed to the store's `load()` method.
76117 * @accessor
76118 */
76119 autoLoad: null,
76120
76121 /**
76122 * @cfg {Boolean} autoSync
76123 * `true` to automatically sync the Store with its Proxy after every edit to one of its Records.
76124 * @accessor
76125 */
76126 autoSync: false,
76127
76128 /**
76129 * @cfg {String} model
76130 * Returns Ext.data.Model and not a String.
76131 * Name of the {@link Ext.data.Model Model} associated with this store.
76132 * The string is used as an argument for {@link Ext.ModelManager#getModel}.
76133 * @accessor
76134 */
76135 model: undefined,
76136
76137 /**
76138 * @cfg {String/Ext.data.proxy.Proxy/Object} proxy The Proxy to use for this Store. This can be either a string, a config
76139 * object or a Proxy instance - see {@link #setProxy} for details.
76140 * @accessor
76141 */
76142 proxy: undefined,
76143
76144 /**
76145 * @cfg {Object[]/Ext.util.Collection} fields
76146 * Returns Ext.util.Collection not just an Object.
76147 * Use in place of specifying a {@link #model} configuration. The fields should be a
76148 * set of {@link Ext.data.Field} configuration objects. The store will automatically create a {@link Ext.data.Model}
76149 * with these fields. In general this configuration option should be avoided, it exists for the purposes of
76150 * backwards compatibility. For anything more complicated, such as specifying a particular id property or
76151 * associations, a {@link Ext.data.Model} should be defined and specified for the {@link #model}
76152 * config.
76153 * @return Ext.util.Collection
76154 * @accessor
76155 */
76156 fields: null,
76157
76158 /**
76159 * @cfg {Boolean} remoteSort
76160 * `true` to defer any sorting operation to the server. If `false`, sorting is done locally on the client.
76161 *
76162 * If this is set to `true`, you will have to manually call the {@link #method-load} method after you {@link #method-sort}, to retrieve the sorted
76163 * data from the server.
76164 *
76165 * {@link #buffered Buffered} stores automatically set this to `true`. Buffered stores contain an abitrary
76166 * subset of the full dataset which depends upon various configurations and which pages have been requested
76167 * for rendering. Such *sparse* datasets are ineligible for local sorting.
76168 * @accessor
76169 */
76170 remoteSort: false,
76171
76172 /**
76173 * @cfg {Boolean} remoteFilter
76174 * `true` to defer any filtering operation to the server. If `false`, filtering is done locally on the client.
76175 *
76176 * If this is set to `true`, you will have to manually call the {@link #method-load} method after you {@link #method-filter} to retrieve the filtered
76177 * data from the server.
76178 *
76179 * {@link #buffered Buffered} stores automatically set this to `true`. Buffered stores contain an abitrary
76180 * subset of the full dataset which depends upon various configurations and which pages have been requested
76181 * for rendering. Such *sparse* datasets are ineligible for local filtering.
76182 * @accessor
76183 */
76184 remoteFilter: false,
76185
76186 /**
76187 * @cfg {Boolean} remoteGroup
76188 * `true` to defer any grouping operation to the server. If `false`, grouping is done locally on the client.
76189 *
76190 * {@link #buffered Buffered} stores automatically set this to `true`. Buffered stores contain an abitrary
76191 * subset of the full dataset which depends upon various configurations and which pages have been requested
76192 * for rendering. Such *sparse* datasets are ineligible for local grouping.
76193 * @accessor
76194 */
76195 remoteGroup: false,
76196
76197 /**
76198 * @cfg {Object[]} filters
76199 * Array of {@link Ext.util.Filter Filters} for this store. This configuration is handled by the
76200 * {@link Ext.mixin.Filterable Filterable} mixin of the {@link Ext.util.Collection data} collection.
76201 * @accessor
76202 */
76203 filters: null,
76204
76205 /**
76206 * @cfg {Object[]} sorters
76207 * Array of {@link Ext.util.Sorter Sorters} for this store. This configuration is handled by the
76208 * {@link Ext.mixin.Sortable Sortable} mixin of the {@link Ext.util.Collection data} collection.
76209 * See also the {@link #sort} method.
76210 * @accessor
76211 */
76212 sorters: null,
76213
76214 /**
76215 * @cfg {Object} grouper
76216 * A configuration object for this Store's {@link Ext.util.Grouper grouper}.
76217 *
76218 * For example, to group a store's items by the first letter of the last name:
76219 *
76220 * Ext.define('People', {
76221 * extend: 'Ext.data.Store',
76222 *
76223 * config: {
76224 * fields: ['first_name', 'last_name'],
76225 *
76226 * grouper: {
76227 * groupFn: function(record) {
76228 * return record.get('last_name').substr(0, 1);
76229 * },
76230 * sortProperty: 'last_name'
76231 * }
76232 * }
76233 * });
76234 *
76235 * @accessor
76236 */
76237 grouper: null,
76238
76239 /**
76240 * @cfg {String} groupField
76241 * The (optional) field by which to group data in the store. Internally, grouping is very similar to sorting - the
76242 * groupField and {@link #groupDir} are injected as the first sorter (see {@link #sort}). Stores support a single
76243 * level of grouping, and groups can be fetched via the {@link #getGroups} method.
76244 * @accessor
76245 */
76246 groupField: null,
76247
76248 /**
76249 * @cfg {String} groupDir
76250 * The direction in which sorting should be applied when grouping. If you specify a grouper by using the {@link #groupField}
76251 * configuration, this will automatically default to "ASC" - the other supported value is "DESC"
76252 * @accessor
76253 */
76254 groupDir: null,
76255
76256 /**
76257 * @cfg {Function} getGroupString This function will be passed to the {@link #grouper} configuration as it's `groupFn`.
76258 * Note that this configuration is deprecated and grouper: `{groupFn: yourFunction}}` is preferred.
76259 * @deprecated
76260 * @accessor
76261 */
76262 getGroupString: null,
76263
76264 /**
76265 * @cfg {Number} pageSize
76266 * The number of records considered to form a 'page'. This is used to power the built-in
76267 * paging using the nextPage and previousPage functions.
76268 *
76269 * If this Store is {@link #buffered}, pages are loaded into a page cache before the Store's
76270 * data is updated from the cache. The pageSize is the number of rows loaded into the cache in one request.
76271 * This will not affect the rendering of a buffered grid, but a larger page size will mean fewer loads.
76272 *
76273 * In a buffered grid, scrolling is monitored, and the page cache is kept primed with data ahead of the
76274 * direction of scroll to provide rapid access to data when scrolling causes it to be required. Several pages
76275 * in advance may be requested depending on various parameters.
76276 *
76277 * It is recommended to tune the {@link #pageSize}, {@link #trailingBufferZone} and
76278 * {@link #leadingBufferZone} configurations based upon the conditions pertaining in your deployed application.
76279 */
76280 pageSize: 25,
76281
76282 /**
76283 * @cfg {Number} totalCount The total number of records in the full dataset, as indicated by a server. If the
76284 * server-side dataset contains 5000 records but only returns pages of 50 at a time, `totalCount` will be set to
76285 * 5000 and {@link #getCount} will return 50
76286 */
76287 totalCount: null,
76288
76289 /**
76290 * @cfg {Boolean} clearOnPageLoad `true` to empty the store when loading another page via {@link #loadPage},
76291 * {@link #nextPage} or {@link #previousPage}. Setting to `false` keeps existing records, allowing
76292 * large data sets to be loaded one page at a time but rendered all together.
76293 * @accessor
76294 */
76295 clearOnPageLoad: true,
76296
76297 /**
76298 * @cfg {Object} params Parameters to send into the proxy for any CRUD operations
76299 *
76300 * @accessor
76301 */
76302 params: {},
76303
76304 modelDefaults: {},
76305
76306 /**
76307 * @cfg {Boolean} autoDestroy This is a private configuration used in the framework whether this Store
76308 * can be destroyed.
76309 * @private
76310 */
76311 autoDestroy: false,
76312
76313 /**
76314 * @cfg {Boolean} syncRemovedRecords This configuration allows you to disable the synchronization of
76315 * removed records on this Store. By default, when you call `removeAll()` or `remove()`, records will be added
76316 * to an internal removed array. When you then sync the Store, we send a destroy request for these records.
76317 * If you don't want this to happen, you can set this configuration to `false`.
76318 */
76319 syncRemovedRecords: true,
76320
76321 /**
76322 * @cfg {Boolean} destroyRemovedRecords This configuration allows you to prevent destroying record
76323 * instances when they are removed from this store and are not in any other store.
76324 */
76325 destroyRemovedRecords: true,
76326
76327 /**
76328 * @cfg {Boolean} buffered
76329 * Allows the Store to prefetch and cache in a **page cache**, pages of Records, and to then satisfy
76330 * loading requirements from this page cache.
76331 *
76332 * To use buffered Stores, initiate the process by loading the first page. The number of rows rendered are
76333 * determined automatically, and the range of pages needed to keep the cache primed for scrolling is
76334 * requested and cached.
76335 * Example:
76336 *
76337 * myStore.loadPage(1); // Load page 1
76338 *
76339 * A {@link Ext.grid.plugin.BufferedRenderer BufferedRenderer} is instantiated which will monitor the scrolling in the grid, and
76340 * refresh the view's rows from the page cache as needed. It will also pull new data into the page
76341 * cache when scrolling of the view draws upon data near either end of the prefetched data.
76342 *
76343 * The margins which trigger view refreshing from the prefetched data are {@link Ext.grid.plugin.BufferedRenderer#numFromEdge},
76344 * {@link Ext.grid.plugin.BufferedRenderer#leadingBufferZone} and {@link Ext.grid.plugin.BufferedRenderer#trailingBufferZone}.
76345 *
76346 * The margins which trigger loading more data into the page cache are, {@link #leadingBufferZone} and
76347 * {@link #trailingBufferZone}.
76348 *
76349 * By default, only 5 pages of data are cached in the page cache, with pages "scrolling" out of the buffer
76350 * as the view moves down through the dataset.
76351 * Setting this value to zero means that no pages are *ever* scrolled out of the page cache, and
76352 * that eventually the whole dataset may become present in the page cache. This is sometimes desirable
76353 * as long as datasets do not reach astronomical proportions.
76354 *
76355 * Selection state may be maintained across page boundaries by configuring the SelectionModel not to discard
76356 * records from its collection when those Records cycle out of the Store's primary collection. This is done
76357 * by configuring the SelectionModel like this:
76358 *
76359 * selModel: {
76360 * pruneRemoved: false
76361 * }
76362 *
76363 */
76364 buffered: false,
76365
76366 /**
76367 * @cfg {Object/Array} plugins
76368 * @accessor
76369 * An object or array of objects that will provide custom functionality for this component. The only
76370 * requirement for a valid plugin is that it contain an init method that accepts a reference of type Ext.Component.
76371 *
76372 * When a component is created, if any plugins are available, the component will call the init method on each
76373 * plugin, passing a reference to itself. Each plugin can then call methods or respond to events on the
76374 * component as needed to provide its functionality.
76375 *
76376 * For examples of plugins, see Ext.plugin.PullRefresh and Ext.plugin.ListPaging
76377 *
76378 * ## Example code
76379 *
76380 * A plugin by alias:
76381 *
76382 * Ext.create('Ext.dataview.List', {
76383 * config: {
76384 * plugins: 'listpaging',
76385 * itemTpl: '<div class="item">{title}</div>',
76386 * store: 'Items'
76387 * }
76388 * });
76389 *
76390 * Multiple plugins by alias:
76391 *
76392 * Ext.create('Ext.dataview.List', {
76393 * config: {
76394 * plugins: ['listpaging', 'pullrefresh'],
76395 * itemTpl: '<div class="item">{title}</div>',
76396 * store: 'Items'
76397 * }
76398 * });
76399 *
76400 * Single plugin by class name with config options:
76401 *
76402 * Ext.create('Ext.dataview.List', {
76403 * config: {
76404 * plugins: {
76405 * xclass: 'Ext.plugin.ListPaging', // Reference plugin by class
76406 * autoPaging: true
76407 * },
76408 *
76409 * itemTpl: '<div class="item">{title}</div>',
76410 * store: 'Items'
76411 * }
76412 * });
76413 *
76414 * Multiple plugins by class name with config options:
76415 *
76416 * Ext.create('Ext.dataview.List', {
76417 * config: {
76418 * plugins: [
76419 * {
76420 * xclass: 'Ext.plugin.PullRefresh',
76421 * pullRefreshText: 'Pull to refresh...'
76422 * },
76423 * {
76424 * xclass: 'Ext.plugin.ListPaging',
76425 * autoPaging: true
76426 * }
76427 * ],
76428 *
76429 * itemTpl: '<div class="item">{title}</div>',
76430 * store: 'Items'
76431 * }
76432 * });
76433 *
76434 */
76435 plugins: null
76436 },
76437
76438 /**
76439 * @property {Number} currentPage
76440 * The page that the Store has most recently loaded (see {@link #loadPage})
76441 */
76442 currentPage: 1,
76443
76444 constructor: function(config) {
76445 config = config || {};
76446
76447 this.data = this._data = this.createDataCollection();
76448
76449 this.data.setSortRoot('data');
76450 this.data.setFilterRoot('data');
76451
76452 this.removed = [];
76453
76454 if (config.id && !config.storeId) {
76455 config.storeId = config.id;
76456 delete config.id;
76457 }
76458
76459
76460 this.initConfig(config);
76461
76462 this.callParent(arguments);
76463 },
76464
76465 applyPlugins: function(config) {
76466 var ln, i, configObj;
76467
76468 if (!config) {
76469 return config;
76470 }
76471
76472 config = [].concat(config);
76473
76474 for (i = 0, ln = config.length; i < ln; i++) {
76475 configObj = config[i];
76476 config[i] = Ext.factory(configObj, 'Ext.plugin.Plugin', null, 'plugin');
76477 }
76478
76479 return config;
76480 },
76481
76482 updatePlugins: function(newPlugins, oldPlugins) {
76483 var ln, i;
76484
76485 if (newPlugins) {
76486 for (i = 0, ln = newPlugins.length; i < ln; i++) {
76487 newPlugins[i].init(this);
76488 }
76489 }
76490
76491 if (oldPlugins) {
76492 for (i = 0, ln = oldPlugins.length; i < ln; i++) {
76493 Ext.destroy(oldPlugins[i]);
76494 }
76495 }
76496 },
76497
76498 /**
76499 * @private
76500 * @return {Ext.util.Collection}
76501 */
76502 createDataCollection: function() {
76503 return new Ext.util.Collection(function(record) {
76504 return record.getId();
76505 });
76506 },
76507
76508 applyStoreId: function(storeId) {
76509 if (storeId === undefined || storeId === null) {
76510 storeId = this.getUniqueId();
76511 }
76512 return storeId;
76513 },
76514
76515 updateStoreId: function(storeId, oldStoreId) {
76516 if (oldStoreId) {
76517 Ext.data.StoreManager.unregister(this);
76518 }
76519 if (storeId) {
76520 Ext.data.StoreManager.register(this);
76521 }
76522 },
76523
76524 applyModel: function(model) {
76525 if (typeof model == 'string') {
76526 var registeredModel = Ext.data.ModelManager.getModel(model);
76527 if (!registeredModel) {
76528 Ext.Logger.error('Model with name "' + model + '" does not exist.');
76529 }
76530 model = registeredModel;
76531 }
76532
76533 if (model && !model.prototype.isModel && Ext.isObject(model)) {
76534 model = Ext.data.ModelManager.registerType(model.storeId || model.id || Ext.id(), model);
76535 }
76536
76537 if (!model) {
76538 var fields = this.getFields(),
76539 data = this.config.data;
76540
76541 if (!fields && data && data.length) {
76542 fields = Ext.Object.getKeys(data[0]);
76543 }
76544
76545 if (fields) {
76546 model = Ext.define('Ext.data.Store.ImplicitModel-' + (this.getStoreId() || Ext.id()), {
76547 extend: 'Ext.data.Model',
76548 config: {
76549 fields: fields,
76550 useCache: false,
76551 proxy: this.getProxy()
76552 }
76553 });
76554
76555 this.implicitModel = true;
76556 }
76557 }
76558 if (!model && this.getProxy()) {
76559 model = this.getProxy().getModel();
76560 }
76561
76562 // <debug>
76563 if (!model) {
76564 Ext.Logger.warn('Unless you define your model through metadata, a store needs to have a model defined on either itself or on its proxy');
76565 }
76566 // </debug>
76567
76568 return model;
76569 },
76570
76571 updateModel: function(model) {
76572 var proxy = this.getProxy();
76573
76574 if (proxy && !proxy.getModel()) {
76575 proxy.setModel(model);
76576 }
76577 },
76578
76579 applyProxy: function(proxy, currentProxy) {
76580 proxy = Ext.factory(proxy, Ext.data.Proxy, currentProxy, 'proxy');
76581
76582 if (!proxy && this.getModel()) {
76583 proxy = this.getModel().getProxy();
76584 }
76585
76586 if (!proxy) {
76587 proxy = new Ext.data.proxy.Memory({
76588 model: this.getModel()
76589 });
76590 }
76591
76592 if (proxy.isMemoryProxy) {
76593 this.setSyncRemovedRecords(false);
76594 }
76595
76596 return proxy;
76597 },
76598
76599 updateProxy: function(proxy, oldProxy) {
76600 if (proxy) {
76601 if (!proxy.getModel()) {
76602 proxy.setModel(this.getModel());
76603 }
76604 proxy.on('metachange', 'onMetaChange', this);
76605 }
76606 if (oldProxy) {
76607 proxy.un('metachange', 'onMetaChange', this);
76608 }
76609 },
76610
76611 /**
76612 * We are using applyData so that we can return nothing and prevent the `this.data`
76613 * property to be overridden.
76614 * @param {Array/Object} data
76615 */
76616 applyData: function(data) {
76617 var me = this,
76618 proxy;
76619 if (data) {
76620 proxy = me.getProxy();
76621 if (proxy instanceof Ext.data.proxy.Memory) {
76622 proxy.setData(data);
76623 me.load();
76624 return;
76625 } else {
76626 // We make it silent because we don't want to fire a refresh event
76627 me.removeAll(true);
76628
76629 // This means we have to fire a clear event though
76630 me.fireEvent('clear', me);
76631
76632 // We don't want to fire addrecords event since we will be firing
76633 // a refresh event later which will already take care of updating
76634 // any views bound to this store
76635 me.suspendEvents();
76636 me.add(data);
76637 me.resumeEvents();
76638
76639 // We set this to true so isAutoLoading to try
76640 me.dataLoaded = true;
76641 }
76642 } else {
76643 me.removeAll(true);
76644
76645 // This means we have to fire a clear event though
76646 me.fireEvent('clear', me);
76647 }
76648
76649 me.fireEvent('refresh', me, me.data);
76650 },
76651
76652 clearData: function() {
76653 this.setData(null);
76654 },
76655
76656 /**
76657 * Uses the configured {@link Ext.data.reader.Reader reader} to convert the data into records
76658 * and adds it to the Store. Use this when you have raw data that needs to pass trough converters,
76659 * mappings and other extra logic from the reader.
76660 *
76661 * If your data is already formated and ready for consumption, use {@link #add} method.
76662 *
76663 * @param {Object[]} data Array of data to load
76664 */
76665 addData: function(data) {
76666 var reader = this.getProxy().getReader(),
76667 resultSet = reader.read(data),
76668 records = resultSet.getRecords();
76669
76670 this.add(records);
76671 },
76672
76673 updateAutoLoad: function(autoLoad) {
76674 var proxy = this.getProxy();
76675 if (autoLoad && (proxy && !proxy.isMemoryProxy)) {
76676 this.load(Ext.isObject(autoLoad) ? autoLoad : null);
76677 }
76678 },
76679
76680 /**
76681 * Returns `true` if the Store is set to {@link #autoLoad} or is a type which loads upon instantiation.
76682 * @return {Boolean}
76683 */
76684 isAutoLoading: function() {
76685 var proxy = this.getProxy();
76686 return (this.getAutoLoad() || (proxy && proxy.isMemoryProxy) || this.dataLoaded);
76687 },
76688
76689 updateGroupField: function(groupField) {
76690 var grouper = this.getGrouper();
76691 if (groupField) {
76692 if (!grouper) {
76693 this.setGrouper({
76694 property: groupField,
76695 direction: this.getGroupDir() || 'ASC'
76696 });
76697 } else {
76698 grouper.setProperty(groupField);
76699 }
76700 } else if (grouper) {
76701 this.setGrouper(null);
76702 }
76703 },
76704
76705 updateGroupDir: function(groupDir) {
76706 var grouper = this.getGrouper();
76707 if (grouper) {
76708 grouper.setDirection(groupDir);
76709 }
76710 },
76711
76712 applyGetGroupString: function(getGroupStringFn) {
76713 var grouper = this.getGrouper();
76714 if (getGroupStringFn) {
76715 // <debug>
76716 Ext.Logger.warn('Specifying getGroupString on a store has been deprecated. Please use grouper: {groupFn: yourFunction}');
76717 // </debug>
76718
76719 if (grouper) {
76720 grouper.setGroupFn(getGroupStringFn);
76721 } else {
76722 this.setGrouper({
76723 groupFn: getGroupStringFn
76724 });
76725 }
76726 } else if (grouper) {
76727 this.setGrouper(null);
76728 }
76729 },
76730
76731 applyGrouper: function(grouper) {
76732 if (typeof grouper == 'string') {
76733 grouper = {
76734 property: grouper
76735 };
76736 }
76737 else if (typeof grouper == 'function') {
76738 grouper = {
76739 groupFn: grouper
76740 };
76741 }
76742
76743 grouper = Ext.factory(grouper, Ext.util.Grouper);
76744 return grouper;
76745 },
76746
76747 updateGrouper: function(grouper, oldGrouper) {
76748 var data = this.data;
76749 if (oldGrouper) {
76750 data.removeSorter(oldGrouper);
76751 if (!grouper) {
76752 data.getSorters().removeSorter('isGrouper');
76753 }
76754 }
76755 if (grouper) {
76756 data.insertSorter(0, grouper);
76757 if (!oldGrouper) {
76758 data.getSorters().addSorter({
76759 direction: 'DESC',
76760 property: 'isGrouper',
76761 transform: function(value) {
76762 return (value === true) ? 1 : -1;
76763 }
76764 });
76765 }
76766 }
76767 this.fireEvent('refresh', this, data);
76768 },
76769
76770 /**
76771 * This method tells you if this store has a grouper defined on it.
76772 * @return {Boolean} `true` if this store has a grouper defined.
76773 */
76774 isGrouped: function() {
76775 return !!this.getGrouper();
76776 },
76777
76778 updateSorters: function(sorters) {
76779 var grouper = this.getGrouper(),
76780 data = this.data,
76781 autoSort = data.getAutoSort();
76782
76783 // While we remove/add sorters we don't want to automatically sort because we still need
76784 // to apply any field sortTypes as transforms on the Sorters after we have added them.
76785 data.setAutoSort(false);
76786
76787 data.setSorters(sorters);
76788 if (grouper) {
76789 data.insertSorter(0, grouper);
76790 }
76791
76792 this.updateSortTypes();
76793
76794 // Now we put back autoSort on the Collection to the value it had before. If it was
76795 // auto sorted, setting this back will cause it to sort right away.
76796 data.setAutoSort(autoSort);
76797 },
76798
76799 updateSortTypes: function() {
76800 var model = this.getModel(),
76801 fields = model && model.getFields(),
76802 data = this.data;
76803
76804 // We loop over each sorter and set it's transform method to the every field's sortType.
76805 if (fields) {
76806 data.getSorters().each(function(sorter) {
76807 var property = sorter.getProperty(),
76808 field;
76809
76810 if (!sorter.isGrouper && property && !sorter.getTransform()) {
76811 field = fields.get(property);
76812 if (field) {
76813 sorter.setTransform(field.getSortType());
76814 }
76815 }
76816 });
76817 }
76818 },
76819
76820 updateFilters: function(filters) {
76821 this.data.setFilters(filters);
76822 },
76823
76824 /**
76825 * Adds Model instance to the Store. This method accepts either:
76826 *
76827 * - An array of Model instances or Model configuration objects.
76828 * - Any number of Model instance or Model configuration object arguments.
76829 *
76830 * The new Model instances will be added at the end of the existing collection.
76831 *
76832 * Sample usage:
76833 *
76834 * myStore.add({some: 'data2'}, {some: 'other data2'});
76835 *
76836 * Use {@link #addData} method instead if you have raw data that need to pass
76837 * through the data reader.
76838 *
76839 * @param {Ext.data.Model[]/Ext.data.Model...} model An array of Model instances
76840 * or Model configuration objects, or variable number of Model instance or config arguments.
76841 * @return {Ext.data.Model[]} The model instances that were added.
76842 */
76843 add: function(records) {
76844 if (!Ext.isArray(records)) {
76845 records = Array.prototype.slice.call(arguments);
76846 }
76847
76848 return this.insert(this.data.length, records);
76849 },
76850
76851 /**
76852 * Inserts Model instances into the Store at the given index and fires the {@link #add} event.
76853 * See also `{@link #add}`.
76854 * @param {Number} index The start index at which to insert the passed Records.
76855 * @param {Ext.data.Model[]} records An Array of Ext.data.Model objects to add to the cache.
76856 * @return {Object}
76857 */
76858 insert: function(index, records) {
76859 if (!Ext.isArray(records)) {
76860 records = Array.prototype.slice.call(arguments, 1);
76861 }
76862
76863 var me = this,
76864 sync = false,
76865 data = this.data,
76866 ln = records.length,
76867 Model = this.getModel(),
76868 modelDefaults = me.getModelDefaults(),
76869 added = false,
76870 i, record;
76871
76872 records = records.slice();
76873
76874 for (i = 0; i < ln; i++) {
76875 record = records[i];
76876 if (!record.isModel) {
76877 record = new Model(record);
76878 }
76879 // If we are adding a record that is already an instance which was still in the
76880 // removed array, then we remove it from the removed array
76881 else if (this.removed.indexOf(record) != -1) {
76882 Ext.Array.remove(this.removed, record);
76883 }
76884
76885 record.set(modelDefaults);
76886 record.join(me);
76887
76888 records[i] = record;
76889
76890 // If this is a newly created record, then we might want to sync it later
76891 sync = sync || (record.phantom === true);
76892 }
76893
76894 // Now we insert all these records in one go to the collection. Saves many function
76895 // calls to data.insert. Does however create two loops over the records we are adding.
76896 if (records.length === 1) {
76897 added = data.insert(index, records[0]);
76898 if (added) {
76899 added = [added];
76900 }
76901 } else {
76902 added = data.insertAll(index, records);
76903 }
76904
76905 if (added) {
76906 me.fireEvent('addrecords', me, added);
76907 }
76908
76909 if (me.getAutoSync() && sync) {
76910 me.sync();
76911 }
76912
76913 return records;
76914 },
76915
76916 /**
76917 * Removes the given record from the Store, firing the `removerecords` event passing all the instances that are removed.
76918 * @param {Ext.data.Model/Ext.data.Model[]} records Model instance or array of instances to remove.
76919 */
76920 remove: function (records) {
76921 if (records.isModel) {
76922 records = [records];
76923 }
76924
76925 var me = this,
76926 sync = false,
76927 i = 0,
76928 autoSync = this.getAutoSync(),
76929 syncRemovedRecords = me.getSyncRemovedRecords(),
76930 destroyRemovedRecords = this.getDestroyRemovedRecords(),
76931 ln = records.length,
76932 indices = [],
76933 removed = [],
76934 isPhantom,
76935 items = me.data.items,
76936 record, index;
76937
76938 for (; i < ln; i++) {
76939 record = records[i];
76940
76941 if (me.data.contains(record)) {
76942 isPhantom = (record.phantom === true);
76943
76944 index = items.indexOf(record);
76945 if (index !== -1) {
76946 removed.push(record);
76947 indices.push(index);
76948 }
76949
76950 record.unjoin(me);
76951 me.data.remove(record);
76952
76953 if (destroyRemovedRecords && !syncRemovedRecords && !record.stores.length) {
76954 record.destroy();
76955 }
76956 else if (!isPhantom && syncRemovedRecords) {
76957 // don't push phantom records onto removed
76958 me.removed.push(record);
76959 }
76960
76961 sync = sync || !isPhantom;
76962 }
76963 }
76964
76965 me.fireEvent('removerecords', me, removed, indices);
76966
76967 if (autoSync && sync) {
76968 me.sync();
76969 }
76970 },
76971
76972 /**
76973 * Removes the model instance at the given index.
76974 * @param {Number} index The record index.
76975 */
76976 removeAt: function(index) {
76977 var record = this.getAt(index);
76978
76979 if (record) {
76980 this.remove(record);
76981 }
76982 },
76983
76984 /**
76985 * Remove all items from the store.
76986 * @param {Boolean} [silent] Prevent the `clear` event from being fired.
76987 */
76988 removeAll: function(silent) {
76989 if (silent !== true && this.eventFiringSuspended !== true) {
76990 this.fireAction('clear', [this], 'doRemoveAll');
76991 } else {
76992 this.doRemoveAll.call(this, true);
76993 }
76994 },
76995
76996 doRemoveAll: function (silent) {
76997 var me = this,
76998 destroyRemovedRecords = this.getDestroyRemovedRecords(),
76999 syncRemovedRecords = this.getSyncRemovedRecords(),
77000 records = me.data.all.slice(),
77001 ln = records.length,
77002 i, record;
77003
77004 for (i = 0; i < ln; i++) {
77005 record = records[i];
77006 record.unjoin(me);
77007
77008 if (destroyRemovedRecords && !syncRemovedRecords && !record.stores.length) {
77009 record.destroy();
77010 }
77011 else if (record.phantom !== true && syncRemovedRecords) {
77012 me.removed.push(record);
77013 }
77014 }
77015
77016 me.data.clear();
77017
77018 if (silent !== true) {
77019 me.fireEvent('refresh', me, me.data);
77020 }
77021
77022 if (me.getAutoSync()) {
77023 this.sync();
77024 }
77025 },
77026
77027 /**
77028 * Calls the specified function for each of the {@link Ext.data.Model Records} in the cache.
77029 *
77030 * // Set up a model to use in our Store
77031 * Ext.define('User', {
77032 * extend: 'Ext.data.Model',
77033 * config: {
77034 * fields: [
77035 * {name: 'firstName', type: 'string'},
77036 * {name: 'lastName', type: 'string'}
77037 * ]
77038 * }
77039 * });
77040 *
77041 * var store = Ext.create('Ext.data.Store', {
77042 * model: 'User',
77043 * data : [
77044 * {firstName: 'Ed', lastName: 'Spencer'},
77045 * {firstName: 'Tommy', lastName: 'Maintz'},
77046 * {firstName: 'Aaron', lastName: 'Conran'},
77047 * {firstName: 'Jamie', lastName: 'Avins'}
77048 * ]
77049 * });
77050 *
77051 * store.each(function (item, index, length) {
77052 * console.log(item.get('firstName'), index);
77053 * });
77054 *
77055 * @param {Function} fn The function to call. Returning `false` aborts and exits the iteration.
77056 * @param {Ext.data.Model} fn.item
77057 * @param {Number} fn.index
77058 * @param {Number} fn.length
77059 * @param {Object} scope (optional) The scope (`this` reference) in which the function is executed.
77060 * Defaults to the current {@link Ext.data.Model Record} in the iteration.
77061 */
77062 each: function(fn, scope) {
77063 this.data.each(fn, scope);
77064 },
77065
77066 /**
77067 * Gets the number of cached records. Note that filtered records are not included in this count.
77068 * If using paging, this may not be the total size of the dataset.
77069 * @return {Number} The number of Records in the Store's cache.
77070 */
77071 getCount: function() {
77072 return this.data.items.length || 0;
77073 },
77074
77075 /**
77076 * Gets the number of all cached records including the ones currently filtered.
77077 * If using paging, this may not be the total size of the dataset.
77078 * @return {Number} The number of all Records in the Store's cache.
77079 */
77080 getAllCount: function () {
77081 return this.data.all.length || 0;
77082 },
77083
77084 /**
77085 * Get the Record at the specified index.
77086 * @param {Number} index The index of the Record to find.
77087 * @return {Ext.data.Model/undefined} The Record at the passed index. Returns `undefined` if not found.
77088 */
77089 getAt: function(index) {
77090 return this.data.getAt(index);
77091 },
77092
77093 /**
77094 * Returns a range of Records between specified indices. Note that if the store is filtered, only filtered results
77095 * are returned.
77096 * @param {Number} [startIndex=0] (optional) The starting index.
77097 * @param {Number} [endIndex=-1] (optional) The ending index (defaults to the last Record in the Store).
77098 * @return {Ext.data.Model[]} An array of Records.
77099 */
77100 getRange: function(start, end) {
77101 return this.data.getRange(start, end);
77102 },
77103
77104 /**
77105 * Get the Record with the specified id.
77106 * @param {String} id The id of the Record to find.
77107 * @return {Ext.data.Model/undefined} The Record with the passed id. Returns `undefined` if not found.
77108 */
77109 getById: function(id) {
77110 return this.data.findBy(function(record) {
77111 return record.getId() == id;
77112 });
77113 },
77114
77115 /**
77116 * Get the index within the cache of the passed Record.
77117 * @param {Ext.data.Model} record The Ext.data.Model object to find.
77118 * @return {Number} The index of the passed Record. Returns -1 if not found.
77119 */
77120 indexOf: function(record) {
77121 return this.data.indexOf(record);
77122 },
77123
77124 /**
77125 * Get the index within the cache of the Record with the passed id.
77126 * @param {String} id The id of the Record to find.
77127 * @return {Number} The index of the Record. Returns -1 if not found.
77128 */
77129 indexOfId: function(id) {
77130 return this.data.indexOfKey(id);
77131 },
77132
77133 /**
77134 * @private
77135 * A model instance should call this method on the Store it has been {@link Ext.data.Model#join joined} to.
77136 * @param {Ext.data.Model} record The model instance that was edited.
77137 * @param {String[]} modifiedFieldNames Array of field names changed during edit.
77138 * @param {Object} modified
77139 */
77140 afterEdit: function(record, modifiedFieldNames, modified) {
77141 var me = this,
77142 data = me.data,
77143 currentId = modified[record.getIdProperty()] || record.getId(),
77144 currentIndex = data.keys.indexOf(currentId),
77145 newIndex;
77146
77147 if (currentIndex === -1 && data.map[currentId] === undefined) {
77148 return;
77149 }
77150
77151 if (me.getAutoSync()) {
77152 me.sync();
77153 }
77154
77155 if (currentId !== record.getId()) {
77156 data.replace(currentId, record);
77157 } else {
77158 data.replace(record);
77159 }
77160
77161 newIndex = data.indexOf(record);
77162 if (currentIndex === -1 && newIndex !== -1) {
77163 me.fireEvent('addrecords', me, [record]);
77164 }
77165 else if (currentIndex !== -1 && newIndex === -1) {
77166 me.fireEvent('removerecords', me, [record], [currentIndex]);
77167 }
77168 else if (newIndex !== -1) {
77169 me.fireEvent('updaterecord', me, record, newIndex, currentIndex, modifiedFieldNames, modified);
77170 }
77171 },
77172
77173 /**
77174 * @private
77175 * A model instance should call this method on the Store it has been {@link Ext.data.Model#join joined} to.
77176 * @param {Ext.data.Model} record The model instance that was edited.
77177 */
77178 afterReject: function(record) {
77179 var index = this.data.indexOf(record);
77180 this.fireEvent('updaterecord', this, record, index, index, [], {});
77181 },
77182
77183 /**
77184 * @private
77185 * A model instance should call this method on the Store it has been {@link Ext.data.Model#join joined} to.
77186 * @param {Ext.data.Model} record The model instance that was edited.
77187 * @param {String[]} modifiedFieldNames
77188 * @param {Object} modified
77189 */
77190 afterCommit: function(record, modifiedFieldNames, modified) {
77191 var me = this,
77192 data = me.data,
77193 currentId = modified[record.getIdProperty()] || record.getId(),
77194 currentIndex = data.keys.indexOf(currentId),
77195 newIndex;
77196
77197 if (currentIndex === -1 && data.map[currentId] === undefined) {
77198 return;
77199 }
77200
77201 if (currentId !== record.getId()) {
77202 data.replace(currentId, record);
77203 } else {
77204 data.replace(record);
77205 }
77206
77207 newIndex = data.indexOf(record);
77208 if (currentIndex === -1 && newIndex !== -1) {
77209 me.fireEvent('addrecords', me, [record]);
77210 }
77211 else if (currentIndex !== -1 && newIndex === -1) {
77212 me.fireEvent('removerecords', me, [record], [currentIndex]);
77213 }
77214 else if (newIndex !== -1) {
77215 me.fireEvent('updaterecord', me, record, newIndex, currentIndex, modifiedFieldNames, modified);
77216 }
77217 },
77218
77219 /**
77220 * This gets called by a record after is gets erased from the server.
77221 * @param {Ext.data.Model} record
77222 * @private
77223 */
77224 afterErase: function(record) {
77225 var me = this,
77226 data = me.data,
77227 index = data.indexOf(record);
77228
77229 if (index !== -1) {
77230 data.remove(record);
77231 me.fireEvent('removerecords', me, [record], [index]);
77232 }
77233 },
77234
77235 applyRemoteFilter: function(value) {
77236 var proxy = this.getProxy();
77237 return value || (proxy && proxy.isSQLProxy === true);
77238 },
77239
77240 applyRemoteSort: function(value) {
77241 var proxy = this.getProxy();
77242 return value || (proxy && proxy.isSQLProxy === true);
77243 },
77244
77245 applyRemoteGroup: function(value) {
77246 var proxy = this.getProxy();
77247 return value || (proxy && proxy.isSQLProxy === true);
77248 },
77249
77250 updateRemoteFilter: function(remoteFilter) {
77251 this.data.setAutoFilter(!remoteFilter);
77252 },
77253
77254 updateRemoteSort: function(remoteSort) {
77255 this.data.setAutoSort(!remoteSort);
77256 },
77257
77258 /**
77259 * Sorts the data in the Store by one or more of its properties. Example usage:
77260 *
77261 * // sort by a single field
77262 * myStore.sort('myField', 'DESC');
77263 *
77264 * // sorting by multiple fields
77265 * myStore.sort([
77266 * {
77267 * property : 'age',
77268 * direction: 'ASC'
77269 * },
77270 * {
77271 * property : 'name',
77272 * direction: 'DESC'
77273 * }
77274 * ]);
77275 *
77276 * Internally, Store converts the passed arguments into an array of {@link Ext.util.Sorter} instances, and delegates
77277 * the actual sorting to its internal {@link Ext.util.Collection}.
77278 *
77279 * When passing a single string argument to sort, Store maintains a ASC/DESC toggler per field, so this code:
77280 *
77281 * store.sort('myField');
77282 * store.sort('myField');
77283 *
77284 * is equivalent to this code:
77285 *
77286 * store.sort('myField', 'ASC');
77287 * store.sort('myField', 'DESC');
77288 *
77289 * because Store handles the toggling automatically.
77290 *
77291 * If the {@link #remoteSort} configuration has been set to `true`, you will have to manually call the {@link #method-load}
77292 * method after you sort to retrieve the sorted data from the server.
77293 *
77294 * @param {String/Ext.util.Sorter[]} sorters Either a string name of one of the fields in this Store's configured
77295 * {@link Ext.data.Model Model}, or an array of sorter configurations.
77296 * @param {String} [defaultDirection=ASC] The default overall direction to sort the data by.
77297 * @param {String} where (Optional) This can be either `'prepend'` or `'append'`. If you leave this undefined
77298 * it will clear the current sorters.
77299 */
77300 sort: function(sorters, defaultDirection, where) {
77301 var data = this.data,
77302 grouper = this.getGrouper(),
77303 autoSort = data.getAutoSort();
77304
77305 if (sorters) {
77306 // While we are adding sorters we don't want to sort right away
77307 // since we need to update sortTypes on the sorters.
77308 data.setAutoSort(false);
77309 if (typeof where === 'string') {
77310 if (where == 'prepend') {
77311 data.insertSorters(grouper ? 1 : 0, sorters, defaultDirection);
77312 } else {
77313 data.addSorters(sorters, defaultDirection);
77314 }
77315 } else {
77316 data.setSorters(null);
77317 if (grouper) {
77318 data.addSorters(grouper);
77319 }
77320 data.addSorters(sorters, defaultDirection);
77321 }
77322 this.updateSortTypes();
77323 // Setting back autoSort to true (if it was like that before) will
77324 // instantly sort the data again.
77325 data.setAutoSort(autoSort);
77326 }
77327
77328 if (!this.getRemoteSort()) {
77329 // If we haven't added any new sorters we have to manually call sort
77330 if (!sorters) {
77331 this.data.sort();
77332 }
77333
77334 this.fireEvent('sort', this, this.data, this.data.getSorters());
77335 if (data.length) {
77336 this.fireEvent('refresh', this, this.data);
77337 }
77338 }
77339 },
77340
77341 /**
77342 * Filters the loaded set of records by a given set of filters.
77343 *
77344 * Filtering by single field:
77345 *
77346 * store.filter("email", /\.com$/);
77347 *
77348 * Using multiple filters:
77349 *
77350 * store.filter([
77351 * {property: "email", value: /\.com$/},
77352 * {filterFn: function(item) { return item.get("age") > 10; }}
77353 * ]);
77354 *
77355 * Using Ext.util.Filter instances instead of config objects
77356 * (note that we need to specify the {@link Ext.util.Filter#root root} config option in this case):
77357 *
77358 * store.filter([
77359 * Ext.create('Ext.util.Filter', {property: "email", value: /\.com$/, root: 'data'}),
77360 * Ext.create('Ext.util.Filter', {filterFn: function(item) { return item.get("age") > 10; }, root: 'data'})
77361 * ]);
77362 *
77363 * If the {@link #remoteFilter} configuration has been set to `true`, you will have to manually call the {@link #method-load}
77364 * method after you filter to retrieve the filtered data from the server.
77365 *
77366 * @param {Object[]/Ext.util.Filter[]/String} filters The set of filters to apply to the data.
77367 * These are stored internally on the store, but the filtering itself is done on the Store's
77368 * {@link Ext.util.MixedCollection MixedCollection}. See MixedCollection's
77369 * {@link Ext.util.MixedCollection#filter filter} method for filter syntax.
77370 * Alternatively, pass in a property string.
77371 * @param {String} [value] value to filter by (only if using a property string as the first argument).
77372 * @param {Boolean} [anyMatch=false] `true` to allow any match, false to anchor regex beginning with `^`.
77373 * @param {Boolean} [caseSensitive=false] `true` to make the filtering regex case sensitive.
77374 */
77375 filter: function(property, value, anyMatch, caseSensitive) {
77376 var data = this.data,
77377 filter = null;
77378
77379 if (property) {
77380 if (Ext.isFunction(property)) {
77381 filter = {filterFn: property};
77382 }
77383 else if (Ext.isArray(property) || property.isFilter) {
77384 filter = property;
77385 }
77386 else {
77387 filter = {
77388 property : property,
77389 value : value,
77390 anyMatch : anyMatch,
77391 caseSensitive: caseSensitive,
77392 id : property
77393 };
77394 }
77395 }
77396
77397 if (this.getRemoteFilter()) {
77398 data.addFilters(filter);
77399 } else {
77400 data.filter(filter);
77401 this.fireEvent('filter', this, data, data.getFilters());
77402 this.fireEvent('refresh', this, data);
77403 }
77404 },
77405
77406 /**
77407 * Filter by a function. The specified function will be called for each
77408 * Record in this Store. If the function returns `true` the Record is included,
77409 * otherwise it is filtered out.
77410 * @param {Function} fn The function to be called. It will be passed the following parameters:
77411 * @param {Ext.data.Model} fn.record The {@link Ext.data.Model record}
77412 * to test for filtering. Access field values using {@link Ext.data.Model#get}.
77413 * @param {Object} fn.id The ID of the Record passed.
77414 * @param {Object} scope (optional) The scope (`this` reference) in which the function is executed. Defaults to this Store.
77415 */
77416 filterBy: function(fn, scope) {
77417 var me = this,
77418 data = me.data,
77419 ln = data.length;
77420
77421 data.filter({
77422 filterFn: function(record) {
77423 return fn.call(scope || me, record, record.getId());
77424 }
77425 });
77426
77427 this.fireEvent('filter', this, data, data.getFilters());
77428
77429 if (data.length !== ln) {
77430 this.fireEvent('refresh', this, data);
77431 }
77432 },
77433
77434 /**
77435 * Query the cached records in this Store using a filtering function. The specified function
77436 * will be called with each record in this Store. If the function returns `true` the record is
77437 * included in the results.
77438 * @param {Function} fn The function to be called. It will be passed the following parameters:
77439 * @param {Ext.data.Model} fn.record The {@link Ext.data.Model record}
77440 * to test for filtering. Access field values using {@link Ext.data.Model#get}.
77441 * @param {Object} fn.id The ID of the Record passed.
77442 * @param {Object} scope (optional) The scope (`this` reference) in which the function is executed. Defaults to this Store.
77443 * @return {Ext.util.MixedCollection} Returns an Ext.util.MixedCollection of the matched records.
77444 */
77445 queryBy: function(fn, scope) {
77446 return this.data.filterBy(fn, scope || this);
77447 },
77448
77449 /**
77450 * Reverts to a view of the Record cache with no filtering applied.
77451 * @param {Boolean} [suppressEvent=false] `true` to clear silently without firing the `refresh` event.
77452 */
77453 clearFilter: function(suppressEvent) {
77454 var ln = this.data.length;
77455 if (suppressEvent) {
77456 this.suspendEvents();
77457 }
77458 this.data.setFilters(null);
77459 if (suppressEvent) {
77460 this.resumeEvents(true);
77461 } else if (ln !== this.data.length) {
77462 this.fireEvent('refresh', this, this.data);
77463 }
77464 },
77465
77466 /**
77467 * Returns `true` if this store is currently filtered.
77468 * @return {Boolean}
77469 */
77470 isFiltered : function () {
77471 return this.data.filtered;
77472 },
77473
77474 /**
77475 * Returns `true` if this store is currently sorted.
77476 * @return {Boolean}
77477 */
77478 isSorted : function () {
77479 return this.data.sorted;
77480 },
77481
77482 getSorters: function() {
77483 var sorters = this.data.getSorters();
77484 return (sorters) ? sorters.items : [];
77485 },
77486
77487 getFilters: function() {
77488 var filters = this.data.getFilters();
77489 return (filters) ? filters.items : [];
77490 },
77491
77492 /**
77493 * Returns an array containing the result of applying the grouper to the records in this store. See {@link #groupField},
77494 * {@link #groupDir} and {@link #grouper}. Example for a store containing records with a color field:
77495 *
77496 * var myStore = Ext.create('Ext.data.Store', {
77497 * groupField: 'color',
77498 * groupDir : 'DESC'
77499 * });
77500 *
77501 * myStore.getGroups(); //returns:
77502 * [
77503 * {
77504 * name: 'yellow',
77505 * children: [
77506 * //all records where the color field is 'yellow'
77507 * ]
77508 * },
77509 * {
77510 * name: 'red',
77511 * children: [
77512 * //all records where the color field is 'red'
77513 * ]
77514 * }
77515 * ]
77516 *
77517 * @param {String} groupName (Optional) Pass in an optional `groupName` argument to access a specific group as defined by {@link #grouper}.
77518 * @return {Object/Object[]} The grouped data.
77519 */
77520 getGroups: function(requestGroupString) {
77521 var records = this.data.items,
77522 length = records.length,
77523 grouper = this.getGrouper(),
77524 groups = [],
77525 pointers = {},
77526 record,
77527 groupStr,
77528 group,
77529 i;
77530
77531 // <debug>
77532 if (!grouper) {
77533 Ext.Logger.error('Trying to get groups for a store that has no grouper');
77534 }
77535 // </debug>
77536
77537 for (i = 0; i < length; i++) {
77538 record = records[i];
77539 groupStr = grouper.getGroupString(record);
77540 group = pointers[groupStr];
77541
77542 if (group === undefined) {
77543 group = {
77544 name: groupStr,
77545 children: []
77546 };
77547
77548 groups.push(group);
77549 pointers[groupStr] = group;
77550 }
77551
77552 group.children.push(record);
77553 }
77554
77555 return requestGroupString ? pointers[requestGroupString] : groups;
77556 },
77557
77558 /**
77559 * @param {Ext.data.Model} record
77560 * @return {null}
77561 */
77562 getGroupString: function(record) {
77563 var grouper = this.getGrouper();
77564 if (grouper) {
77565 return grouper.getGroupString(record);
77566 }
77567 return null;
77568 },
77569
77570 /**
77571 * Finds the index of the first matching Record in this store by a specific field value.
77572 * @param {String} fieldName The name of the Record field to test.
77573 * @param {String/RegExp} value Either a string that the field value
77574 * should begin with, or a RegExp to test against the field.
77575 * @param {Number} startIndex (optional) The index to start searching at.
77576 * @param {Boolean} anyMatch (optional) `true` to match any part of the string, not just the beginning.
77577 * @param {Boolean} caseSensitive (optional) `true` for case sensitive comparison.
77578 * @param {Boolean} [exactMatch=false] (optional) `true` to force exact match (^ and $ characters added to the regex).
77579 * @return {Number} The matched index or -1
77580 */
77581 find: function(fieldName, value, startIndex, anyMatch, caseSensitive, exactMatch) {
77582 var filter = Ext.create('Ext.util.Filter', {
77583 property: fieldName,
77584 value: value,
77585 anyMatch: anyMatch,
77586 caseSensitive: caseSensitive,
77587 exactMatch: exactMatch,
77588 root: 'data'
77589 });
77590 return this.data.findIndexBy(filter.getFilterFn(), null, startIndex);
77591 },
77592
77593 /**
77594 * Finds the first matching Record in this store by a specific field value.
77595 * @param {String} fieldName The name of the Record field to test.
77596 * @param {String/RegExp} value Either a string that the field value
77597 * should begin with, or a RegExp to test against the field.
77598 * @param {Number} startIndex (optional) The index to start searching at.
77599 * @param {Boolean} anyMatch (optional) `true` to match any part of the string, not just the beginning.
77600 * @param {Boolean} caseSensitive (optional) `true` for case sensitive comparison.
77601 * @param {Boolean} [exactMatch=false] (optional) `true` to force exact match (^ and $ characters added to the regex).
77602 * @return {Ext.data.Model} The matched record or `null`.
77603 */
77604 findRecord: function() {
77605 var me = this,
77606 index = me.find.apply(me, arguments);
77607 return index !== -1 ? me.getAt(index) : null;
77608 },
77609
77610 /**
77611 * Finds the index of the first matching Record in this store by a specific field value.
77612 * @param {String} fieldName The name of the Record field to test.
77613 * @param {Object} value The value to match the field against.
77614 * @param {Number} startIndex (optional) The index to start searching at.
77615 * @return {Number} The matched index or -1.
77616 */
77617 findExact: function(fieldName, value, startIndex) {
77618 return this.data.findIndexBy(function(record) {
77619 return record.get(fieldName) === value;
77620 }, this, startIndex);
77621 },
77622
77623 /**
77624 * Find the index of the first matching Record in this Store by a function.
77625 * If the function returns `true` it is considered a match.
77626 * @param {Function} fn The function to be called. It will be passed the following parameters:
77627 * @param {Ext.data.Model} fn.record The {@link Ext.data.Model record}
77628 * to test for filtering. Access field values using {@link Ext.data.Model#get}.
77629 * @param {Object} fn.id The ID of the Record passed.
77630 * @param {Object} scope (optional) The scope (`this` reference) in which the function is executed. Defaults to this Store.
77631 * @param {Number} startIndex (optional) The index to start searching at.
77632 * @return {Number} The matched index or -1.
77633 */
77634 findBy: function(fn, scope, startIndex) {
77635 return this.data.findIndexBy(fn, scope, startIndex);
77636 },
77637
77638 /**
77639 * Loads data into the Store via the configured {@link #proxy}. This uses the Proxy to make an
77640 * asynchronous call to whatever storage backend the Proxy uses, automatically adding the retrieved
77641 * instances into the Store and calling an optional callback if required. Example usage:
77642 *
77643 * store.load({
77644 * callback: function(records, operation, success) {
77645 * // the {@link Ext.data.Operation operation} object contains all of the details of the load operation
77646 * console.log(records);
77647 * },
77648 * scope: this
77649 * });
77650 *
77651 * If only the callback and scope options need to be specified, then one can call it simply like so:
77652 *
77653 * store.load(function(records, operation, success) {
77654 * console.log('loaded records');
77655 * }, this);
77656 *
77657 * @param {Object/Function} [options] config object, passed into the {@link Ext.data.Operation} object before loading.
77658 * @param {Object} [scope] Scope for the function.
77659 * @return {Object}
77660 */
77661 load: function(options, scope) {
77662 var me = this,
77663 operation,
77664 currentPage = me.currentPage,
77665 pageSize = me.getPageSize();
77666
77667 options = options || {};
77668
77669 if (Ext.isFunction(options)) {
77670 options = {
77671 callback: options,
77672 scope: scope || this
77673 };
77674 }
77675
77676 if (me.getRemoteSort()) {
77677 options.sorters = options.sorters || this.getSorters();
77678 }
77679
77680 if (me.getRemoteFilter()) {
77681 options.filters = options.filters || this.getFilters();
77682 }
77683
77684 if (me.getRemoteGroup()) {
77685 options.grouper = options.grouper || this.getGrouper();
77686 }
77687
77688 Ext.applyIf(options, {
77689 page: currentPage,
77690 start: (currentPage - 1) * pageSize,
77691 limit: pageSize,
77692 addRecords: false,
77693 action: 'read',
77694 params: this.getParams(),
77695 model: this.getModel()
77696 });
77697
77698 operation = Ext.create('Ext.data.Operation', options);
77699
77700 if (me.fireEvent('beforeload', me, operation) !== false) {
77701 me.loading = true;
77702 me.getProxy().read(operation, me.onProxyLoad, me);
77703 }
77704
77705 return me;
77706 },
77707
77708 /**
77709 * Returns `true` if the Store is currently performing a load operation.
77710 * @return {Boolean} `true` if the Store is currently loading.
77711 */
77712 isLoading: function() {
77713 return Boolean(this.loading);
77714 },
77715
77716 /**
77717 * Returns `true` if the Store has been loaded.
77718 * @return {Boolean} `true` if the Store has been loaded.
77719 */
77720 isLoaded: function() {
77721 return Boolean(this.loaded);
77722 },
77723
77724 /**
77725 * Synchronizes the Store with its Proxy. This asks the Proxy to batch together any new, updated
77726 * and deleted records in the store, updating the Store's internal representation of the records
77727 * as each operation completes.
77728 * @return {Object}
77729 * @return {Object} return.added
77730 * @return {Object} return.updated
77731 * @return {Object} return.removed
77732 */
77733 sync: function(options) {
77734 var me = this,
77735 operations = {},
77736 toCreate = me.getNewRecords(),
77737 toUpdate = me.getUpdatedRecords(),
77738 toDestroy = me.getRemovedRecords(),
77739 needsSync = false;
77740
77741 if (toCreate.length > 0) {
77742 operations.create = toCreate;
77743 needsSync = true;
77744 }
77745
77746 if (toUpdate.length > 0) {
77747 operations.update = toUpdate;
77748 needsSync = true;
77749 }
77750
77751 if (toDestroy.length > 0) {
77752 operations.destroy = toDestroy;
77753 needsSync = true;
77754 }
77755
77756 if (needsSync && me.fireEvent('beforesync', this, operations) !== false) {
77757 me.getProxy().batch(Ext.merge({
77758 operations: operations,
77759 listeners: me.getBatchListeners()
77760 }, options || {}));
77761 }
77762
77763 return {
77764 added: toCreate,
77765 updated: toUpdate,
77766 removed: toDestroy
77767 };
77768 },
77769
77770 /**
77771 * Convenience function for getting the first model instance in the store.
77772 * @return {Ext.data.Model/undefined} The first model instance in the store, or `undefined`.
77773 */
77774 first: function() {
77775 return this.data.first();
77776 },
77777
77778 /**
77779 * Convenience function for getting the last model instance in the store.
77780 * @return {Ext.data.Model/undefined} The last model instance in the store, or `undefined`.
77781 */
77782 last: function() {
77783 return this.data.last();
77784 },
77785
77786 /**
77787 * Sums the value of `property` for each {@link Ext.data.Model record} between `start`
77788 * and `end` and returns the result.
77789 * @param {String} field The field in each record.
77790 * @return {Number} The sum.
77791 */
77792 sum: function(field) {
77793 var total = 0,
77794 i = 0,
77795 records = this.data.items,
77796 len = records.length;
77797
77798 for (; i < len; ++i) {
77799 total += records[i].get(field);
77800 }
77801
77802 return total;
77803 },
77804
77805 /**
77806 * Gets the minimum value in the store.
77807 * @param {String} field The field in each record.
77808 * @return {Object/undefined} The minimum value, if no items exist, `undefined`.
77809 */
77810 min: function(field) {
77811 var i = 1,
77812 records = this.data.items,
77813 len = records.length,
77814 value, min;
77815
77816 if (len > 0) {
77817 min = records[0].get(field);
77818 }
77819
77820 for (; i < len; ++i) {
77821 value = records[i].get(field);
77822 if (value < min) {
77823 min = value;
77824 }
77825 }
77826 return min;
77827 },
77828
77829 /**
77830 * Gets the maximum value in the store.
77831 * @param {String} field The field in each record.
77832 * @return {Object/undefined} The maximum value, if no items exist, `undefined`.
77833 */
77834 max: function(field) {
77835 var i = 1,
77836 records = this.data.items,
77837 len = records.length,
77838 value,
77839 max;
77840
77841 if (len > 0) {
77842 max = records[0].get(field);
77843 }
77844
77845 for (; i < len; ++i) {
77846 value = records[i].get(field);
77847 if (value > max) {
77848 max = value;
77849 }
77850 }
77851 return max;
77852 },
77853
77854 /**
77855 * Gets the average value in the store.
77856 * @param {String} field The field in each record you want to get the average for.
77857 * @return {Number} The average value, if no items exist, 0.
77858 */
77859 average: function(field) {
77860 var i = 0,
77861 records = this.data.items,
77862 len = records.length,
77863 sum = 0;
77864
77865 if (records.length > 0) {
77866 for (; i < len; ++i) {
77867 sum += records[i].get(field);
77868 }
77869 return sum / len;
77870 }
77871 return 0;
77872 },
77873
77874 /**
77875 * @private
77876 * Returns an object which is passed in as the listeners argument to `proxy.batch` inside `this.sync`.
77877 * This is broken out into a separate function to allow for customization of the listeners.
77878 * @return {Object} The listeners object.
77879 * @return {Object} return.scope
77880 * @return {Object} return.exception
77881 * @return {Object} return.complete
77882 */
77883 getBatchListeners: function() {
77884 return {
77885 scope: this,
77886 exception: this.onBatchException,
77887 complete: this.onBatchComplete
77888 };
77889 },
77890
77891 /**
77892 * @private
77893 * Attached as the `complete` event listener to a proxy's Batch object. Iterates over the batch operations
77894 * and updates the Store's internal data MixedCollection.
77895 */
77896 onBatchComplete: function(batch) {
77897 var me = this,
77898 operations = batch.operations,
77899 length = operations.length,
77900 i;
77901
77902 for (i = 0; i < length; i++) {
77903 me.onProxyWrite(operations[i]);
77904 }
77905 },
77906
77907 onBatchException: function(batch, operation) {
77908 // //decide what to do... could continue with the next operation
77909 // batch.start();
77910 //
77911 // //or retry the last operation
77912 // batch.retry();
77913 },
77914
77915 /**
77916 * @private
77917 * Called internally when a Proxy has completed a load request.
77918 */
77919 onProxyLoad: function(operation) {
77920 var me = this,
77921 records = operation.getRecords(),
77922 resultSet = operation.getResultSet(),
77923 successful = operation.wasSuccessful();
77924
77925 if (resultSet) {
77926 me.setTotalCount(resultSet.getTotal());
77927 }
77928
77929 if (successful) {
77930 this.fireAction('datarefresh', [this, this.data, operation], 'doDataRefresh');
77931 }
77932
77933 me.loaded = true;
77934 me.loading = false;
77935 me.fireEvent('load', this, records, successful, operation);
77936
77937 //this is a callback that would have been passed to the 'read' function and is optional
77938 Ext.callback(operation.getCallback(), operation.getScope() || me, [records, operation, successful]);
77939 },
77940
77941 doDataRefresh: function(store, data, operation) {
77942 var records = operation.getRecords(),
77943 me = this,
77944 destroyRemovedRecords = me.getDestroyRemovedRecords(),
77945 currentRecords = data.all.slice(),
77946 ln = currentRecords.length,
77947 ln2 = records.length,
77948 ids = {},
77949 i, record;
77950
77951 if (operation.getAddRecords() !== true) {
77952 for (i = 0; i < ln2; i++) {
77953 ids[records[i].id] = true;
77954 }
77955 for (i = 0; i < ln; i++) {
77956 record = currentRecords[i];
77957 record.unjoin(me);
77958
77959 // If the record we are removing is not part of the records we are about to add to the store then handle
77960 // the destroying or removing of the record to avoid memory leaks.
77961 if (ids[record.id] !== true && destroyRemovedRecords && !record.stores.length) {
77962 record.destroy();
77963 }
77964 }
77965
77966 data.clear();
77967 // This means we have to fire a clear event though
77968 me.fireEvent('clear', me);
77969 }
77970 if (records && records.length) {
77971 // Now lets add the records without firing an addrecords event
77972 me.suspendEvents();
77973 me.add(records);
77974 me.resumeEvents(true); // true to discard the queue
77975 }
77976
77977 me.fireEvent('refresh', me, data);
77978 },
77979
77980 /**
77981 * @private
77982 * Callback for any write Operation over the Proxy. Updates the Store's MixedCollection to reflect
77983 * the updates provided by the Proxy.
77984 */
77985 onProxyWrite: function(operation) {
77986 var me = this,
77987 success = operation.wasSuccessful(),
77988 records = operation.getRecords();
77989
77990 switch (operation.getAction()) {
77991 case 'create':
77992 me.onCreateRecords(records, operation, success);
77993 break;
77994 case 'update':
77995 me.onUpdateRecords(records, operation, success);
77996 break;
77997 case 'destroy':
77998 me.onDestroyRecords(records, operation, success);
77999 break;
78000 }
78001
78002 if (success) {
78003 me.fireEvent('write', me, operation);
78004 }
78005 //this is a callback that would have been passed to the 'create', 'update' or 'destroy' function and is optional
78006 Ext.callback(operation.getCallback(), operation.getScope() || me, [records, operation, success]);
78007 },
78008
78009 // These methods are now just template methods since updating the records etc is all taken care of
78010 // by the operation itself.
78011 onCreateRecords: function(records, operation, success) {},
78012 onUpdateRecords: function(records, operation, success) {},
78013
78014 onDestroyRecords: function(records, operation, success) {
78015 this.removed = [];
78016 },
78017
78018 onMetaChange: function(data) {
78019 var model = this.getProxy().getModel();
78020 if (!this.getModel() && model) {
78021 this.setModel(model);
78022 }
78023
78024 /**
78025 * @event metachange
78026 * Fires whenever the server has sent back new metadata to reconfigure the Reader.
78027 * @param {Ext.data.Store} this
78028 * @param {Object} data The metadata sent back from the server.
78029 */
78030 this.fireEvent('metachange', this, data);
78031 },
78032
78033 /**
78034 * Returns all Model instances that are either currently a phantom (e.g. have no id), or have an ID but have not
78035 * yet been saved on this Store (this happens when adding a non-phantom record from another Store into this one).
78036 * @return {Ext.data.Model[]} The Model instances.
78037 */
78038 getNewRecords: function() {
78039 return this.data.filterBy(function(item) {
78040 // only want phantom records that are valid
78041 return item.phantom === true && item.isValid();
78042 }).items;
78043 },
78044
78045 /**
78046 * Returns all Model instances that have been updated in the Store but not yet synchronized with the Proxy.
78047 * @return {Ext.data.Model[]} The updated Model instances.
78048 */
78049 getUpdatedRecords: function() {
78050 return this.data.filterBy(function(item) {
78051 // only want dirty records, not phantoms that are valid
78052 return item.dirty === true && item.phantom !== true && item.isValid();
78053 }).items;
78054 },
78055
78056 /**
78057 * Returns any records that have been removed from the store but not yet destroyed on the proxy.
78058 * @return {Ext.data.Model[]} The removed Model instances.
78059 */
78060 getRemovedRecords: function() {
78061 return this.removed;
78062 },
78063
78064 // PAGING METHODS
78065 /**
78066 * Loads a given 'page' of data by setting the start and limit values appropriately. Internally this just causes a normal
78067 * load operation, passing in calculated `start` and `limit` params.
78068 * @param {Number} page The number of the page to load.
78069 * @param {Object} options See options for {@link #method-load}.
78070 * @param {Object} scope
78071 */
78072 loadPage: function(page, options, scope) {
78073 if (typeof options === 'function') {
78074 options = {
78075 callback: options,
78076 scope: scope || this
78077 };
78078
78079 }
78080 var me = this,
78081 pageSize = me.getPageSize(),
78082 clearOnPageLoad = me.getClearOnPageLoad();
78083
78084 options = Ext.apply({}, options);
78085
78086 me.currentPage = page;
78087
78088 me.load(Ext.applyIf(options, {
78089 page: page,
78090 start: (page - 1) * pageSize,
78091 limit: pageSize,
78092 addRecords: !clearOnPageLoad
78093 }));
78094 },
78095
78096 /**
78097 * Loads the next 'page' in the current data set.
78098 * @param {Object} options See options for {@link #method-load}.
78099 */
78100 nextPage: function(options) {
78101 this.loadPage(this.currentPage + 1, options);
78102 },
78103
78104 /**
78105 * Loads the previous 'page' in the current data set.
78106 * @param {Object} options See options for {@link #method-load}.
78107 */
78108 previousPage: function(options) {
78109 this.loadPage(this.currentPage - 1, options);
78110 },
78111
78112 destroy: function() {
78113 this.clearData();
78114 var proxy = this.getProxy();
78115 if (proxy) {
78116 proxy.onDestroy();
78117 }
78118 Ext.data.StoreManager.unregister(this);
78119
78120 Ext.destroy(this.getPlugins());
78121
78122 if (this.implicitModel && this.getModel()) {
78123 delete Ext.data.ModelManager.types[this.getModel().getName()];
78124 }
78125 Ext.destroy(this.data);
78126
78127 this.callParent(arguments);
78128 }
78129
78130 });
78131
78132 /**
78133 * Exports an SVG document to an image. To do this,
78134 * the SVG string must be sent to a remote server and processed.
78135 *
78136 * # Sending the data
78137 *
78138 * A post request is made to the URL. The following fields are sent:
78139 *
78140 * + width: The width of the image
78141 * + height: The height of the image
78142 * + type: The image type to save as, see {@link #supportedTypes}
78143 * + svg: The svg string for the surface
78144 *
78145 * # The response
78146 *
78147 * It is expected that the user will be prompted with an image download.
78148 * As such, the following options should be set on the server:
78149 *
78150 * + Content-Disposition: 'attachment, filename="chart.png"'
78151 * + Content-Type: 'image/png'
78152 *
78153 * **Important**: By default, chart data is sent to a server operated
78154 * by Sencha to do data processing. You may change this default by
78155 * setting the {@link #defaultUrl} of this class.
78156 * In addition, please note that this service only creates PNG images.
78157 */
78158 // TODO: can't use the canvas element to convert SVG to bitmap on the client, see:
78159 // TODO: http://stackoverflow.com/questions/18586808/canvas-todatauri-on-chrome-security-issue
78160 Ext.define('Ext.draw.engine.SvgExporter', {
78161 singleton: true,
78162
78163 /**
78164 * @property {String} [defaultUrl="http://svg.sencha.io"]
78165 * The default URL to submit the form request.
78166 */
78167 defaultUrl: 'http://svg.sencha.io',
78168
78169 /**
78170 * @property {Array} [supportedTypes=["image/png", "image/jpeg"]]
78171 * A list of export types supported by the server
78172 */
78173 supportedTypes: ['image/png', 'image/jpeg'],
78174
78175 /**
78176 * @property {String} [widthParam="width"]
78177 * The name of the width parameter to be sent to the server.
78178 * The Sencha IO server expects it to be the default value.
78179 */
78180 widthParam: 'width',
78181
78182 /**
78183 * @property {String} [heightParam="height"]
78184 * The name of the height parameter to be sent to the server.
78185 * The Sencha IO server expects it to be the default value.
78186 */
78187 heightParam: 'height',
78188
78189 /**
78190 * @property {String} [typeParam="type"]
78191 * The name of the type parameter to be sent to the server.
78192 * The Sencha IO server expects it to be the default value.
78193 */
78194 typeParam: 'type',
78195
78196 /**
78197 * @property {String} [svgParam="svg"]
78198 * The name of the svg parameter to be sent to the server.
78199 * The Sencha IO server expects it to be the default value.
78200 */
78201 svgParam: 'svg',
78202
78203 formCls: Ext.baseCSSPrefix + 'hide-display',
78204
78205 /**
78206 * Exports the surface to an image
78207 * @param {String} svg The SVG document.
78208 * @param {Object} [config] The following config options are supported:
78209 *
78210 * @param {Number} config.width A width to send to the server to for
78211 * configuring the image width (required)
78212 *
78213 * @param {Number} config.height A height to send to the server for
78214 * configuring the image height (required)
78215 *
78216 * @param {String} config.url The url to post the data to. Defaults to
78217 * the {@link #defaultUrl} configuration on the class.
78218 *
78219 * @param {String} config.type The type of image to export. See the
78220 * {@link #supportedTypes}
78221 *
78222 * @param {String} config.widthParam The name of the width parameter to send
78223 * to the server. Defaults to {@link #widthParam}
78224 *
78225 * @param {String} config.heightParam The name of the height parameter to send
78226 * to the server. Defaults to {@link #heightParam}
78227 *
78228 * @param {String} config.typeParam The name of the type parameter to send
78229 * to the server. Defaults to {@link #typeParam}
78230 *
78231 * @param {String} config.svgParam The name of the svg parameter to send
78232 * to the server. Defaults to {@link #svgParam}
78233 *
78234 * @return {Boolean} True if the surface was successfully sent to the server.
78235 */
78236 generate: function(svg, config) {
78237 config = config || {};
78238 var me = this,
78239 type = config.type,
78240 form;
78241
78242 if (Ext.Array.indexOf(me.supportedTypes, type) === -1) {
78243 return false;
78244 }
78245
78246 form = Ext.getBody().createChild({
78247 tag: 'form',
78248 method: 'POST',
78249 action: config.url || me.defaultUrl,
78250 cls: me.formCls,
78251 children: [{
78252 tag: 'input',
78253 type: 'hidden',
78254 name: config.widthParam || me.widthParam,
78255 value: config.width
78256 }, {
78257 tag: 'input',
78258 type: 'hidden',
78259 name: config.heightParam || me.heightParam,
78260 value: config.height
78261 }, {
78262 tag: 'input',
78263 type: 'hidden',
78264 name: config.typeParam || me.typeParam,
78265 value: type
78266 }, {
78267 tag: 'input',
78268 type: 'hidden',
78269 name: config.svgParam || me.svgParam
78270 }]
78271 });
78272
78273 // Assign the data on the value so it doesn't get messed up in the html insertion
78274 form.last(null, true).value = svg;
78275
78276 form.dom.submit();
78277 form.remove();
78278 return true;
78279 }
78280
78281 });
78282
78283 /**
78284 * The Ext.chart package provides the capability to visualize data.
78285 * Each chart binds directly to an {@link Ext.data.Store} enabling automatic updates of the chart.
78286 * A chart configuration object has some overall styling options as well as an array of axes
78287 * and series. A chart instance example could look like:
78288 *
78289 * new Ext.chart.CartesianChart({
78290 * width: 800,
78291 * height: 600,
78292 * animate: true,
78293 * store: store1,
78294 * legend: {
78295 * position: 'right'
78296 * },
78297 * axes: [
78298 * // ...some axes options...
78299 * ],
78300 * series: [
78301 * // ...some series options...
78302 * ]
78303 * });
78304 *
78305 * In this example we set the `width` and `height` of a cartesian chart; We decide whether our series are
78306 * animated or not and we select a store to be bound to the chart; We also set the legend to the right part of the
78307 * chart.
78308 *
78309 * You can register certain interactions such as {@link Ext.chart.interactions.PanZoom} on the chart by specify an
78310 * array of names or more specific config objects. All the events will be wired automatically.
78311 *
78312 * You can also listen to `itemXXX` events directly on charts. That case all the contained series will relay this event to the
78313 * chart.
78314 *
78315 * For more information about the axes and series configurations please check the documentation of
78316 * each series (Line, Bar, Pie, etc).
78317 *
78318 */
78319
78320 Ext.define('Ext.chart.AbstractChart', {
78321
78322 extend: Ext.draw.Component ,
78323
78324
78325
78326
78327
78328
78329
78330
78331
78332
78333 /**
78334 * @event beforerefresh
78335 * Fires before a refresh to the chart data is called. If the `beforerefresh` handler returns
78336 * `false` the {@link #refresh} action will be canceled.
78337 * @param {Ext.chart.AbstractChart} this
78338 */
78339
78340 /**
78341 * @event refresh
78342 * Fires after the chart data has been refreshed.
78343 * @param {Ext.chart.AbstractChart} this
78344 */
78345
78346 /**
78347 * @event redraw
78348 * Fires after the chart is redrawn.
78349 * @param {Ext.chart.AbstractChart} this
78350 */
78351
78352 /**
78353 * @event itemmousemove
78354 * Fires when the mouse is moved on a series item.
78355 * @param {Ext.chart.series.Series} series
78356 * @param {Object} item
78357 * @param {Event} event
78358 */
78359 /**
78360 * @event itemmouseup
78361 * Fires when a mouseup event occurs on a series item.
78362 * @param {Ext.chart.series.Series} series
78363 * @param {Object} item
78364 * @param {Event} event
78365 */
78366 /**
78367 * @event itemmousedown
78368 * Fires when a mousedown event occurs on a series item.
78369 * @param {Ext.chart.series.Series} series
78370 * @param {Object} item
78371 * @param {Event} event
78372 */
78373 /**
78374 * @event itemmouseover
78375 * Fires when the mouse enters a series item.
78376 * @param {Ext.chart.series.Series} series
78377 * @param {Object} item
78378 * @param {Event} event
78379 */
78380 /**
78381 * @event itemmouseout
78382 * Fires when the mouse exits a series item.
78383 * @param {Ext.chart.series.Series} series
78384 * @param {Object} item
78385 * @param {Event} event
78386 */
78387 /**
78388 * @event itemclick
78389 * Fires when a click event occurs on a series item.
78390 * @param {Ext.chart.series.Series} series
78391 * @param {Object} item
78392 * @param {Event} event
78393 */
78394 /**
78395 * @event itemdoubleclick
78396 * Fires when a doubleclick event occurs on a series item.
78397 * @param {Ext.chart.series.Series} series
78398 * @param {Object} item
78399 * @param {Event} event
78400 */
78401 /**
78402 * @event itemtap
78403 * Fires when a tap event occurs on a series item.
78404 * @param {Ext.chart.series.Series} series
78405 * @param {Object} item
78406 * @param {Event} event
78407 */
78408 /**
78409 * @event itemtapstart
78410 * Fires when a tapstart event occurs on a series item.
78411 * @param {Ext.chart.series.Series} series
78412 * @param {Object} item
78413 * @param {Event} event
78414 */
78415 /**
78416 * @event itemtapend
78417 * Fires when a tapend event occurs on a series item.
78418 * @param {Ext.chart.series.Series} series
78419 * @param {Object} item
78420 * @param {Event} event
78421 */
78422 /**
78423 * @event itemtapcancel
78424 * Fires when a tapcancel event occurs on a series item.
78425 * @param {Ext.chart.series.Series} series
78426 * @param {Object} item
78427 * @param {Event} event
78428 */
78429 /**
78430 * @event itemtaphold
78431 * Fires when a taphold event occurs on a series item.
78432 * @param {Ext.chart.series.Series} series
78433 * @param {Object} item
78434 * @param {Event} event
78435 */
78436 /**
78437 * @event itemdoubletap
78438 * Fires when a doubletap event occurs on a series item.
78439 * @param {Ext.chart.series.Series} series
78440 * @param {Object} item
78441 * @param {Event} event
78442 */
78443 /**
78444 * @event itemsingletap
78445 * Fires when a singletap event occurs on a series item.
78446 * @param {Ext.chart.series.Series} series
78447 * @param {Object} item
78448 * @param {Event} event
78449 */
78450 /**
78451 * @event itemtouchstart
78452 * Fires when a touchstart event occurs on a series item.
78453 * @param {Ext.chart.series.Series} series
78454 * @param {Object} item
78455 * @param {Event} event
78456 */
78457 /**
78458 * @event itemtouchmove
78459 * Fires when a touchmove event occurs on a series item.
78460 * @param {Ext.chart.series.Series} series
78461 * @param {Object} item
78462 * @param {Event} event
78463 */
78464 /**
78465 * @event itemtouchend
78466 * Fires when a touchend event occurs on a series item.
78467 * @param {Ext.chart.series.Series} series
78468 * @param {Object} item
78469 * @param {Event} event
78470 */
78471 /**
78472 * @event itemdragstart
78473 * Fires when a dragstart event occurs on a series item.
78474 * @param {Ext.chart.series.Series} series
78475 * @param {Object} item
78476 * @param {Event} event
78477 */
78478 /**
78479 * @event itemdrag
78480 * Fires when a drag event occurs on a series item.
78481 * @param {Ext.chart.series.Series} series
78482 * @param {Object} item
78483 * @param {Event} event
78484 */
78485 /**
78486 * @event itemdragend
78487 * Fires when a dragend event occurs on a series item.
78488 * @param {Ext.chart.series.Series} series
78489 * @param {Object} item
78490 * @param {Event} event
78491 */
78492 /**
78493 * @event itempinchstart
78494 * Fires when a pinchstart event occurs on a series item.
78495 * @param {Ext.chart.series.Series} series
78496 * @param {Object} item
78497 * @param {Event} event
78498 */
78499 /**
78500 * @event itempinch
78501 * Fires when a pinch event occurs on a series item.
78502 * @param {Ext.chart.series.Series} series
78503 * @param {Object} item
78504 * @param {Event} event
78505 */
78506 /**
78507 * @event itempinchend
78508 * Fires when a pinchend event occurs on a series item.
78509 * @param {Ext.chart.series.Series} series
78510 * @param {Object} item
78511 * @param {Event} event
78512 */
78513 /**
78514 * @event itemswipe
78515 * Fires when a swipe event occurs on a series item.
78516 * @param {Ext.chart.series.Series} series
78517 * @param {Object} item
78518 * @param {Event} event
78519 */
78520
78521 /**
78522 * @property version Current Version of Touch Charts
78523 * @type {String}
78524 */
78525 version: '2.0.0',
78526
78527 // @ignore
78528 viewBox: false,
78529
78530 delegationRegex: /^item([a-z]+)$/i,
78531
78532 domEvents: /click|focus|blur|paste|input|mousemove|mousedown|mouseup|mouseover|mouseout|keyup|keydown|keypress|submit|pinch|pinchstart|pinchend|touchstart|touchend|rotate|rotatestart|rotateend|drag|dragstart|dragend|tap|doubletap|singletap/,
78533
78534 config: {
78535
78536 /**
78537 * @cfg {Ext.data.Store} store
78538 * The store that supplies data to this chart.
78539 */
78540 store: null,
78541
78542 /**
78543 * @cfg {Boolean/Object} shadow (optional) `true` for the default shadow configuration `{shadowOffsetX: 2, shadowOffsetY: 2, shadowBlur: 3, shadowColor: '#444'}`
78544 * or a standard shadow config object to be used for default chart shadows.
78545 */
78546 shadow: false,
78547
78548 /**
78549 * @cfg {Boolean/Object} animate (optional) `true` for the default animation (easing: 'ease' and duration: 500)
78550 * or a standard animation config object to be used for default chart animations.
78551 */
78552 animate: true,
78553
78554 /**
78555 * @cfg {Ext.chart.series.Series/Array} series
78556 * Array of {@link Ext.chart.series.Series Series} instances or config objects. For example:
78557 *
78558 * series: [{
78559 * type: 'column',
78560 * axis: 'left',
78561 * listeners: {
78562 * 'afterrender': function() {
78563 * console.log('afterrender');
78564 * }
78565 * },
78566 * xField: 'category',
78567 * yField: 'data1'
78568 * }]
78569 */
78570 series: [],
78571
78572 /**
78573 * @cfg {Ext.chart.axis.Axis/Array/Object} axes
78574 * Array of {@link Ext.chart.axis.Axis Axis} instances or config objects. For example:
78575 *
78576 * axes: [{
78577 * type: 'numeric',
78578 * position: 'left',
78579 * title: 'Number of Hits',
78580 * minimum: 0
78581 * }, {
78582 * type: 'category',
78583 * position: 'bottom',
78584 * title: 'Month of the Year'
78585 * }]
78586 */
78587 axes: [],
78588
78589 /**
78590 * @cfg {Ext.chart.Legend/Object} legend
78591 */
78592 legend: null,
78593
78594 /**
78595 * @cfg {Boolean/Array} colors Array of colors/gradients to override the color of items and legends.
78596 */
78597 colors: null,
78598
78599 /**
78600 * @cfg {Object|Number} insetPadding The amount of inset padding in pixels for the chart. Inset padding is
78601 * the padding from the boundary of the chart to any of its contents.
78602 * @cfg {Number} insetPadding.top
78603 */
78604 insetPadding: {
78605 top: 10,
78606 left: 10,
78607 right: 10,
78608 bottom: 10
78609 },
78610
78611 /**
78612 * @cfg {Object} innerPadding The amount of inner padding in pixel. Inner padding is the padding from
78613 * axis to the series.
78614 */
78615 innerPadding: {
78616 top: 0,
78617 left: 0,
78618 right: 0,
78619 bottom: 0
78620 },
78621
78622 /**
78623 * @cfg {Object} background Set the chart background. This can be a gradient object, image, or color.
78624 *
78625 * For example, if `background` were to be a color we could set the object as
78626 *
78627 * background: {
78628 * //color string
78629 * fill: '#ccc'
78630 * }
78631 *
78632 * You can specify an image by using:
78633 *
78634 * background: {
78635 * image: 'http://path.to.image/'
78636 * }
78637 *
78638 * Also you can specify a gradient by using the gradient object syntax:
78639 *
78640 * background: {
78641 * gradient: {
78642 * type: 'linear',
78643 * angle: 45,
78644 * stops: {
78645 * 0: {
78646 * color: '#555'
78647 * },
78648 * 100: {
78649 * color: '#ddd'
78650 * }
78651 * }
78652 * }
78653 * }
78654 */
78655 background: null,
78656
78657 /**
78658 * @cfg {Array} interactions
78659 * Interactions are optional modules that can be plugged in to a chart to allow the user to interact
78660 * with the chart and its data in special ways. The `interactions` config takes an Array of Object
78661 * configurations, each one corresponding to a particular interaction class identified by a `type` property:
78662 *
78663 * new Ext.chart.AbstractChart({
78664 * renderTo: Ext.getBody(),
78665 * width: 800,
78666 * height: 600,
78667 * store: store1,
78668 * axes: [
78669 * // ...some axes options...
78670 * ],
78671 * series: [
78672 * // ...some series options...
78673 * ],
78674 * interactions: [{
78675 * type: 'interactiontype'
78676 * // ...additional configs for the interaction...
78677 * }]
78678 * });
78679 *
78680 * When adding an interaction which uses only its default configuration (no extra properties other than `type`),
78681 * you can alternately specify only the type as a String rather than the full Object:
78682 *
78683 * interactions: ['reset', 'rotate']
78684 *
78685 * The current supported interaction types include:
78686 *
78687 * - {@link Ext.chart.interactions.PanZoom panzoom} - allows pan and zoom of axes
78688 * - {@link Ext.chart.interactions.ItemHighlight itemhighlight} - allows highlighting of series data points
78689 * - {@link Ext.chart.interactions.ItemInfo iteminfo} - allows displaying details of a data point in a popup panel
78690 * - {@link Ext.chart.interactions.Rotate rotate} - allows rotation of pie and radar series
78691 *
78692 * See the documentation for each of those interaction classes to see how they can be configured.
78693 *
78694 * Additional custom interactions can be registered using `'interactions.'` alias prefix.
78695 */
78696 interactions: [],
78697
78698 /**
78699 * @private
78700 * The main region of the chart.
78701 */
78702 mainRegion: null,
78703
78704 /**
78705 * @private
78706 * Override value
78707 */
78708 autoSize: false,
78709
78710 /**
78711 * @private
78712 * Override value
78713 */
78714 viewBox: false,
78715
78716 /**
78717 * @private
78718 * Override value
78719 */
78720 fitSurface: false,
78721
78722 /**
78723 * @private
78724 * Override value
78725 */
78726 resizeHandler: null,
78727
78728 /**
78729 * @readonly
78730 * @cfg {Object} highlightItem
78731 * The current highlight item in the chart.
78732 * The object must be the one that you get from item events.
78733 *
78734 * Note that series can also own highlight items.
78735 * This notion is separate from this one and should not be used at the same time.
78736 */
78737 highlightItem: null
78738 },
78739
78740 /**
78741 * @private
78742 */
78743 resizing: 0,
78744
78745 /**
78746 * Toggle for chart interactions that require animation to be suspended.
78747 * @private
78748 */
78749 animationSuspended: 0,
78750
78751 /**
78752 * @private The z-indexes to use for the various surfaces
78753 */
78754 surfaceZIndexes: {
78755 main: 0,
78756 grid: 1,
78757 series: 2,
78758 axis: 3,
78759 overlay: 4,
78760 events: 5
78761 },
78762
78763 animating: 0,
78764
78765 layoutSuspended: 0,
78766
78767 applyAnimate: function (newAnimate, oldAnimate) {
78768 if (!newAnimate) {
78769 newAnimate = {
78770 duration: 0
78771 };
78772 } else if (newAnimate === true) {
78773 newAnimate = {
78774 easing: 'easeInOut',
78775 duration: 500
78776 };
78777 }
78778 if (!oldAnimate) {
78779 return newAnimate;
78780 } else {
78781 oldAnimate = Ext.apply({}, newAnimate, oldAnimate);
78782 }
78783 return oldAnimate;
78784 },
78785
78786 applyInsetPadding: function (padding, oldPadding) {
78787 if (Ext.isNumber(padding)) {
78788 return {
78789 top: padding,
78790 left: padding,
78791 right: padding,
78792 bottom: padding
78793 };
78794 } else if (!oldPadding) {
78795 return padding;
78796 } else {
78797 return Ext.apply(oldPadding, padding);
78798 }
78799 },
78800
78801 applyInnerPadding: function (padding, oldPadding) {
78802 if (Ext.isNumber(padding)) {
78803 return {
78804 top: padding,
78805 left: padding,
78806 right: padding,
78807 bottom: padding
78808 };
78809 } else if (!oldPadding) {
78810 return padding;
78811 } else {
78812 return Ext.apply(oldPadding, padding);
78813 }
78814 },
78815
78816 suspendAnimation: function () {
78817 this.animationSuspended++;
78818 if (this.animationSuspended === 1) {
78819 var series = this.getSeries(), i = -1, n = series.length;
78820 while (++i < n) {
78821 //update animation config to not animate
78822 series[i].setAnimate(this.getAnimate());
78823 }
78824 }
78825 },
78826
78827 resumeAnimation: function () {
78828 this.animationSuspended--;
78829 if (this.animationSuspended === 0) {
78830 var series = this.getSeries(), i = -1, n = series.length;
78831 while (++i < n) {
78832 //update animation config to animate
78833 series[i].setAnimate(this.getAnimate());
78834 }
78835 }
78836 },
78837
78838 suspendLayout: function () {
78839 this.layoutSuspended++;
78840 if (this.layoutSuspended === 1) {
78841 if (this.scheduledLayoutId) {
78842 this.layoutInSuspension = true;
78843 this.cancelLayout();
78844 } else {
78845 this.layoutInSuspension = false;
78846 }
78847 }
78848 },
78849
78850 resumeLayout: function () {
78851 this.layoutSuspended--;
78852 if (this.layoutSuspended === 0) {
78853 if (this.layoutInSuspension) {
78854 this.scheduleLayout();
78855 }
78856 }
78857 },
78858
78859 /**
78860 * Cancel a scheduled layout.
78861 */
78862 cancelLayout: function () {
78863 if (this.scheduledLayoutId) {
78864 Ext.draw.Animator.cancel(this.scheduledLayoutId);
78865 this.scheduledLayoutId = null;
78866 }
78867 },
78868
78869 /**
78870 * Schedule a layout at next frame.
78871 */
78872 scheduleLayout: function () {
78873 if (!this.scheduledLayoutId) {
78874 this.scheduledLayoutId = Ext.draw.Animator.schedule('doScheduleLayout', this);
78875 }
78876 },
78877
78878 doScheduleLayout: function () {
78879 if (this.layoutSuspended) {
78880 this.layoutInSuspension = true;
78881 } else {
78882 this.performLayout();
78883 }
78884 },
78885
78886 getAnimate: function () {
78887 if (this.resizing || this.animationSuspended) {
78888 return {
78889 duration: 0
78890 };
78891 } else {
78892 return this._animate;
78893 }
78894 },
78895
78896 constructor: function () {
78897 var me = this;
78898 me.itemListeners = {};
78899 me.surfaceMap = {};
78900 me.legendStore = new Ext.data.Store({
78901 storeId: this.getId() + '-legendStore',
78902 autoDestroy: true,
78903 fields: [
78904 'id', 'name', 'mark', 'disabled', 'series', 'index'
78905 ]
78906 });
78907 me.suspendLayout();
78908 me.callSuper(arguments);
78909 me.refreshLegendStore();
78910 me.getLegendStore().on('updaterecord', 'onUpdateLegendStore', me);
78911 me.resumeLayout();
78912 },
78913
78914 /**
78915 * Return the legend store that contains all the legend information. These
78916 * information are collected from all the series.
78917 * @return {Ext.data.Store}
78918 */
78919 getLegendStore: function () {
78920 return this.legendStore;
78921 },
78922
78923 refreshLegendStore: function () {
78924 if (this.getLegendStore()) {
78925 var i, ln,
78926 series = this.getSeries(), seriesItem,
78927 legendData = [];
78928 if (series) {
78929 for (i = 0, ln = series.length; i < ln; i++) {
78930 seriesItem = series[i];
78931 if (seriesItem.getShowInLegend()) {
78932 seriesItem.provideLegendInfo(legendData);
78933 }
78934 }
78935 }
78936 this.getLegendStore().setData(legendData);
78937 }
78938 },
78939
78940 resetLegendStore: function () {
78941 if (this.getLegendStore()) {
78942 var data = this.getLegendStore().getData().items,
78943 i, ln = data.length,
78944 record;
78945 for (i = 0; i < ln; i++) {
78946 record = data[i];
78947 record.beginEdit();
78948 record.set('disabled', false);
78949 record.commit();
78950 }
78951 }
78952 },
78953
78954 onUpdateLegendStore: function (store, record) {
78955 var series = this.getSeries(), seriesItem;
78956 if (record && series) {
78957 seriesItem = series.map[record.get('series')];
78958 if (seriesItem) {
78959 seriesItem.setHiddenByIndex(record.get('index'), record.get('disabled'));
78960 this.redraw();
78961 }
78962 }
78963 },
78964
78965 initialize: function () {
78966 var me = this;
78967 me.callSuper();
78968 me.getSurface('main');
78969 me.getSurface('overlay-surface', 'overlay').waitFor(me.getSurface('series-surface', 'series'));
78970 },
78971
78972 resizeHandler: function (size) {
78973 var me = this;
78974 me.scheduleLayout();
78975 return false;
78976 },
78977
78978 applyMainRegion: function (newRegion, region) {
78979 if (!region) {
78980 return newRegion;
78981 }
78982 this.getSeries();
78983 this.getAxes();
78984 if (newRegion[0] === region[0] &&
78985 newRegion[1] === region[1] &&
78986 newRegion[2] === region[2] &&
78987 newRegion[3] === region[3]) {
78988 return region;
78989 } else {
78990 return newRegion;
78991 }
78992 },
78993
78994 getSurface: function (name, type) {
78995 name = name || 'main';
78996 type = type || name;
78997 var me = this,
78998 surface = this.callSuper([name]),
78999 zIndexes = me.surfaceZIndexes;
79000 if (type in zIndexes) {
79001 surface.element.setStyle('zIndex', zIndexes[type]);
79002 }
79003 if (!me.surfaceMap[type]) {
79004 me.surfaceMap[type] = [];
79005 }
79006 surface.type = type;
79007 me.surfaceMap[type].push(surface);
79008 return surface;
79009 },
79010
79011 updateColors: function (colors) {
79012 var series = this.getSeries(),
79013 seriesCount = series && series.length,
79014 seriesItem;
79015 for (var i = 0; i < seriesCount; i++) {
79016 seriesItem = series[i];
79017 if (!seriesItem.getColors()) {
79018 seriesItem.updateColors(colors);
79019 }
79020 }
79021 },
79022
79023 applyAxes: function (newAxes, oldAxes) {
79024 this.resizing++;
79025 try {
79026 if (!oldAxes) {
79027 oldAxes = [];
79028 oldAxes.map = {};
79029 }
79030 var result = [], i, ln, axis, oldAxis, oldMap = oldAxes.map;
79031 result.map = {};
79032 newAxes = Ext.Array.from(newAxes, true);
79033 for (i = 0, ln = newAxes.length; i < ln; i++) {
79034 axis = newAxes[i];
79035 if (!axis) {
79036 continue;
79037 }
79038 axis = Ext.factory(axis, null, oldAxis = oldMap[axis.getId && axis.getId() || axis.id], 'axis');
79039 if (axis) {
79040 axis.setChart(this);
79041 result.push(axis);
79042 result.map[axis.getId()] = axis;
79043 if (!oldAxis) {
79044 axis.on('animationstart', 'onAnimationStart', this);
79045 axis.on('animationend', 'onAnimationEnd', this);
79046 }
79047 }
79048 }
79049
79050 for (i in oldMap) {
79051 if (!result.map[i]) {
79052 oldMap[i].destroy();
79053 }
79054 }
79055 return result;
79056 } finally {
79057 this.resizing--;
79058 }
79059 },
79060
79061 updateAxes: function (newAxes) {
79062 this.scheduleLayout();
79063 },
79064
79065 applySeries: function (newSeries, oldSeries) {
79066 this.resizing++;
79067 try {
79068 this.getAxes();
79069 if (!oldSeries) {
79070 oldSeries = [];
79071 oldSeries.map = {};
79072 }
79073 var me = this,
79074 result = [],
79075 i, ln, series, oldMap = oldSeries.map, oldSeriesItem;
79076 result.map = {};
79077 newSeries = Ext.Array.from(newSeries, true);
79078 for (i = 0, ln = newSeries.length; i < ln; i++) {
79079 series = newSeries[i];
79080 if (!series) {
79081 continue;
79082 }
79083 oldSeriesItem = oldSeries.map[series.getId && series.getId() || series.id];
79084 if (series instanceof Ext.chart.series.Series) {
79085 if (oldSeriesItem !== series) {
79086 // Replacing
79087 if (oldSeriesItem) {
79088 oldSeriesItem.destroy();
79089 }
79090 me.addItemListenersToSeries(series);
79091 }
79092 series.setChart(this);
79093 } else if (Ext.isObject(series)) {
79094 if (oldSeriesItem) {
79095 // Update
79096 oldSeriesItem.setConfig(series);
79097 series = oldSeriesItem;
79098 } else {
79099 // Create a series.
79100 if (Ext.isString(series)) {
79101 series = Ext.create(series.xclass || ("series." + series), {chart: this});
79102 } else {
79103 series.chart = this;
79104 series = Ext.create(series.xclass || ("series." + series.type), series);
79105 }
79106 series.on('animationstart', 'onAnimationStart', this);
79107 series.on('animationend', 'onAnimationEnd', this);
79108 me.addItemListenersToSeries(series);
79109 }
79110 }
79111
79112 result.push(series);
79113 result.map[series.getId()] = series;
79114 }
79115
79116 for (i in oldMap) {
79117 if (!result.map[oldMap[i].getId()]) {
79118 oldMap[i].destroy();
79119 }
79120 }
79121 return result;
79122 } finally {
79123 this.resizing--;
79124 }
79125 },
79126
79127 applyLegend: function (newLegend, oldLegend) {
79128 return Ext.factory(newLegend, Ext.chart.Legend, oldLegend);
79129 },
79130
79131 updateLegend: function (legend) {
79132 if (legend) {
79133 // On create
79134 legend.setStore(this.getLegendStore());
79135 if (!legend.getDocked()) {
79136 legend.setDocked('bottom');
79137 }
79138 if (this.getParent()) {
79139 this.getParent().add(legend);
79140 }
79141 }
79142 },
79143
79144 setParent: function (parent) {
79145 this.callSuper(arguments);
79146 if (parent && this.getLegend()) {
79147 parent.add(this.getLegend());
79148 }
79149 },
79150
79151 updateSeries: function (newSeries, oldSeries) {
79152 this.resizing++;
79153 try {
79154 this.fireEvent('serieschanged', this, newSeries, oldSeries);
79155 this.refreshLegendStore();
79156 this.scheduleLayout();
79157 } finally {
79158 this.resizing--;
79159 }
79160 },
79161
79162 applyInteractions: function (interactions, oldInteractions) {
79163 if (!oldInteractions) {
79164 oldInteractions = [];
79165 oldInteractions.map = {};
79166 }
79167 var me = this,
79168 result = [], oldMap = oldInteractions.map,
79169 i, ln, interaction;
79170 result.map = {};
79171 interactions = Ext.Array.from(interactions, true);
79172 for (i = 0, ln = interactions.length; i < ln; i++) {
79173 interaction = interactions[i];
79174 if (!interaction) {
79175 continue;
79176 }
79177 interaction = Ext.factory(interaction, null, oldMap[interaction.getId && interaction.getId() || interaction.id], 'interaction');
79178 if (interaction) {
79179 interaction.setChart(me);
79180 result.push(interaction);
79181 result.map[interaction.getId()] = interaction;
79182 }
79183 }
79184
79185 for (i in oldMap) {
79186 if (!result.map[oldMap[i]]) {
79187 oldMap[i].destroy();
79188 }
79189 }
79190 return result;
79191 },
79192
79193 applyStore: function (store) {
79194 return Ext.StoreManager.lookup(store);
79195 },
79196
79197 updateStore: function (newStore, oldStore) {
79198 var me = this;
79199 if (oldStore) {
79200 oldStore.un('refresh', 'onRefresh', me, null, 'after');
79201 if (oldStore.autoDestroy) {
79202 oldStore.destroy();
79203 }
79204 }
79205 if (newStore) {
79206 newStore.on('refresh', 'onRefresh', me, null, 'after');
79207 }
79208
79209 me.fireEvent('storechanged', newStore, oldStore);
79210 me.onRefresh();
79211 },
79212
79213 /**
79214 * Redraw the chart. If animations are set this will animate the chart too.
79215 */
79216 redraw: function () {
79217 this.fireEvent('redraw', this);
79218 },
79219
79220 performLayout: function () {
79221 this.cancelLayout();
79222 },
79223
79224 getEventXY: function (e) {
79225 e = (e.changedTouches && e.changedTouches[0]) || e.event || e.browserEvent || e;
79226 var me = this,
79227 xy = me.element.getXY(),
79228 region = me.getMainRegion();
79229 return [e.pageX - xy[0] - region[0], e.pageY - xy[1] - region[1]];
79230 },
79231
79232 /**
79233 * Given an x/y point relative to the chart, find and return the first series item that
79234 * matches that point.
79235 * @param {Number} x
79236 * @param {Number} y
79237 * @return {Object} An object with `series` and `item` properties, or `false` if no item found.
79238 */
79239 getItemForPoint: function (x, y) {
79240 var me = this,
79241 i = 0,
79242 items = me.getSeries(),
79243 l = items.length,
79244 series, item;
79245
79246 for (; i < l; i++) {
79247 series = items[i];
79248 item = series.getItemForPoint(x, y);
79249 if (item) {
79250 return item;
79251 }
79252 }
79253
79254 return null;
79255 },
79256
79257 /**
79258 * Given an x/y point relative to the chart, find and return all series items that match that point.
79259 * @param {Number} x
79260 * @param {Number} y
79261 * @return {Array} An array of objects with `series` and `item` properties.
79262 */
79263 getItemsForPoint: function (x, y) {
79264 var me = this,
79265 series = me.getSeries(),
79266 seriesItem,
79267 items = [];
79268
79269 for (var i = 0; i < series.length; i++) {
79270 seriesItem = series[i];
79271 var item = seriesItem.getItemForPoint(x, y);
79272 if (item) {
79273 items.push(item);
79274 }
79275 }
79276
79277 return items;
79278 },
79279
79280 /**
79281 * @private
79282 */
79283 delayThicknessChanged: 0,
79284
79285 /**
79286 * @private
79287 */
79288 thicknessChanged: false,
79289
79290 /**
79291 * Suspend the layout initialized by thickness change
79292 */
79293 suspendThicknessChanged: function () {
79294 this.delayThicknessChanged++;
79295 },
79296
79297 /**
79298 * Resume the layout initialized by thickness change
79299 */
79300 resumeThicknessChanged: function () {
79301 this.delayThicknessChanged--;
79302 if (this.delayThicknessChanged === 0 && this.thicknessChanged) {
79303 this.onThicknessChanged();
79304 }
79305 },
79306
79307 onAnimationStart: function () {
79308 this.fireEvent('animationstart', this);
79309 },
79310
79311 onAnimationEnd: function () {
79312 this.fireEvent('animationend', this);
79313 },
79314
79315 onThicknessChanged: function () {
79316 if (this.delayThicknessChanged === 0) {
79317 this.thicknessChanged = false;
79318 this.performLayout();
79319 } else {
79320 this.thicknessChanged = true;
79321 }
79322 },
79323
79324 /**
79325 * @private
79326 */
79327 onRefresh: function () {
79328 var region = this.getMainRegion(),
79329 axes = this.getAxes(),
79330 store = this.getStore(),
79331 series = this.getSeries();
79332 if (!store || !axes || !series || !region) {
79333 return;
79334 }
79335 this.redraw();
79336 },
79337
79338 /**
79339 * Changes the data store bound to this chart and refreshes it.
79340 * @param {Ext.data.Store} store The store to bind to this chart.
79341 */
79342 bindStore: function (store) {
79343 this.setStore(store);
79344 },
79345
79346 applyHighlightItem: function (newHighlightItem, oldHighlightItem) {
79347 if (newHighlightItem === oldHighlightItem) {
79348 return;
79349 }
79350 if (Ext.isObject(newHighlightItem) && Ext.isObject(oldHighlightItem)) {
79351 if (newHighlightItem.sprite === oldHighlightItem.sprite &&
79352 newHighlightItem.index === oldHighlightItem.index
79353 ) {
79354 return;
79355 }
79356 }
79357 return newHighlightItem;
79358 },
79359
79360 updateHighlightItem: function (newHighlightItem, oldHighlightItem) {
79361 if (oldHighlightItem) {
79362 oldHighlightItem.series.setAttributesForItem(oldHighlightItem, {highlighted: false});
79363 }
79364 if (newHighlightItem) {
79365 newHighlightItem.series.setAttributesForItem(newHighlightItem, {highlighted: true});
79366 }
79367 },
79368
79369 addItemListenersToSeries: function (series) {
79370 for (var name in this.itemListeners) {
79371 var listenerMap = this.itemListeners[name], i, ln;
79372 for (i = 0, ln = listenerMap.length; i < ln; i++) {
79373 series.addListener.apply(series, listenerMap[i]);
79374 }
79375 }
79376 },
79377
79378 addItemListener: function (name, fn, scope, options, order) {
79379 var listenerMap = this.itemListeners[name] || (this.itemListeners[name] = []),
79380 series = this.getSeries(), seriesItem,
79381 i, ln;
79382 listenerMap.push([name, fn, scope, options, order]);
79383 if (series) {
79384 for (i = 0, ln = series.length; i < ln; i++) {
79385 seriesItem = series[i];
79386 seriesItem.addListener(name, fn, scope, options, order);
79387 }
79388 }
79389 },
79390
79391 remoteItemListener: function (name, fn, scope, options, order) {
79392 var listenerMap = this.itemListeners[name],
79393 series = this.getSeries(), seriesItem,
79394 i, ln;
79395 if (listenerMap) {
79396 for (i = 0, ln = listenerMap.length; i < ln; i++) {
79397 if (listenerMap[i].fn === fn) {
79398 listenerMap.splice(i, 1);
79399 if (series) {
79400 for (i = 0, ln = series.length; i < ln; i++) {
79401 seriesItem = series[i];
79402 seriesItem.removeListener(name, fn, scope, options, order);
79403 }
79404 }
79405 break;
79406 }
79407 }
79408 }
79409 },
79410
79411 doAddListener: function (name, fn, scope, options, order) {
79412 if (name.match(this.delegationRegex)) {
79413 return this.addItemListener(name, fn, scope || this, options, order);
79414 } else if (name.match(this.domEvents)) {
79415 return this.element.doAddListener.apply(this.element, arguments);
79416 } else {
79417 return this.callSuper(arguments);
79418 }
79419 },
79420
79421 doRemoveListener: function (name, fn, scope, options, order) {
79422 if (name.match(this.delegationRegex)) {
79423 return this.remoteItemListener(name, fn, scope || this, options, order);
79424 } else if (name.match(this.domEvents)) {
79425 return this.element.doRemoveListener.apply(this.element, arguments);
79426 } else {
79427 return this.callSuper(arguments);
79428 }
79429 },
79430
79431 onItemRemove: function (item) {
79432 this.callSuper(arguments);
79433 if (this.surfaceMap) {
79434 Ext.Array.remove(this.surfaceMap[item.type], item);
79435 if (this.surfaceMap[item.type].length === 0) {
79436 delete this.surfaceMap[item.type];
79437 }
79438 }
79439 },
79440
79441 // @private remove gently.
79442 destroy: function () {
79443 var me = this,
79444 emptyArray = [],
79445 legend = me.getLegend(),
79446 legendStore = me.getLegendStore();
79447 me.surfaceMap = null;
79448 me.setHighlightItem(null);
79449 me.setSeries(emptyArray);
79450 me.setAxes(emptyArray);
79451 me.setInteractions(emptyArray);
79452 if (legendStore) {
79453 legendStore.destroy();
79454 me.legendStore = null;
79455 }
79456 if (legend) {
79457 legend.destroy();
79458 me.setLegend(null);
79459 }
79460 me.setStore(null);
79461 Ext.Viewport.un('orientationchange', me.redraw, me);
79462 me.cancelLayout();
79463 this.callSuper(arguments);
79464 },
79465
79466 /* ---------------------------------
79467 Methods needed for ComponentQuery
79468 ----------------------------------*/
79469
79470 /**
79471 * @private
79472 * @param {Boolean} deep
79473 * @return {Array}
79474 */
79475 getRefItems: function (deep) {
79476 var me = this,
79477 series = me.getSeries(),
79478 axes = me.getAxes(),
79479 interaction = me.getInteractions(),
79480 ans = [], i, ln;
79481
79482 for (i = 0, ln = series.length; i < ln; i++) {
79483 ans.push(series[i]);
79484 if (series[i].getRefItems) {
79485 ans.push.apply(ans, series[i].getRefItems(deep));
79486 }
79487 }
79488
79489 for (i = 0, ln = axes.length; i < ln; i++) {
79490 ans.push(axes[i]);
79491 if (axes[i].getRefItems) {
79492 ans.push.apply(ans, axes[i].getRefItems(deep));
79493 }
79494 }
79495
79496 for (i = 0, ln = interaction.length; i < ln; i++) {
79497 ans.push(interaction[i]);
79498 if (interaction[i].getRefItems) {
79499 ans.push.apply(ans, interaction[i].getRefItems(deep));
79500 }
79501 }
79502
79503 return ans;
79504 },
79505
79506 /**
79507 * Flattens the given chart surfaces into a single image.
79508 * Note: Surfaces whose class name is different from chart's engine will be omitted.
79509 * @param {Array} surfaces A list of chart's surfaces to flatten.
79510 * @param {String} format If set to 'image', the method will return an Image object. Otherwise, the dataURL
79511 * of the flattened image will be returned.
79512 * @returns {String|Image} An Image DOM element containing the flattened image or its dataURL.
79513 */
79514 flatten: function (surfaces, format) {
79515 var me = this,
79516 size = me.element.getSize(),
79517 svg, canvas, ctx,
79518 i, surface, region,
79519 img, dataURL;
79520
79521 switch (me.engine) {
79522 case 'Ext.draw.engine.Canvas':
79523 canvas = document.createElement('canvas');
79524 canvas.width = size.width;
79525 canvas.height = size.height;
79526 ctx = canvas.getContext('2d');
79527 for (i = 0; i < surfaces.length; i++) {
79528 surface = surfaces[i];
79529 if (Ext.getClassName(surface) !== me.engine) {
79530 continue;
79531 }
79532 region = surface.getRegion();
79533 ctx.drawImage(surface.canvases[0].dom, region[0], region[1]);
79534 }
79535 dataURL = canvas.toDataURL();
79536 break;
79537 case 'Ext.draw.engine.Svg':
79538 svg = '<?xml version="1.0" standalone="yes"?>';
79539 svg += '<svg version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg"' +
79540 ' width="' + size.width + '"' +
79541 ' height="' + size.height + '">';
79542 for (i = 0; i < surfaces.length - 1; i++) {
79543 surface = surfaces[i];
79544 if (Ext.getClassName(surface) !== me.engine) {
79545 continue;
79546 }
79547 region = surface.getRegion();
79548 svg += '<g transform="translate(' + region[0] + ',' + region[1] + ')">';
79549 svg += Ext.dom.Element.serializeNode(surface.svgElement.dom);
79550 svg += '</g>';
79551 }
79552 svg += '</svg>';
79553 dataURL = 'data:image/svg+xml;utf8,' + svg;
79554 break;
79555 }
79556 if (format === 'image') {
79557 img = new Image();
79558 img.src = dataURL;
79559 return img;
79560 }
79561 if (format === 'stream') {
79562 return dataURL.replace(/^data:image\/[^;]+/, 'data:application/octet-stream');
79563 }
79564 return dataURL;
79565 },
79566
79567 save: function (download) {
79568 if (download) {
79569 // TODO: no control over filename, we are at the browser's mercy
79570 // TODO: unfortunatelly, a.download attribute remains a novelty on mobile: http://caniuse.com/#feat=download
79571 window.open(this.flatten(this.items.items, 'stream'));
79572 } else {
79573 Ext.Viewport.add({
79574 xtype: 'panel',
79575 layout: 'fit',
79576 modal: true,
79577 width: '90%',
79578 height: '90%',
79579 hideOnMaskTap: true,
79580 centered: true,
79581 scrollable: false,
79582 items: {
79583 xtype: 'image',
79584 mode: 'img',
79585 src: this.flatten(this.items.items)
79586 },
79587 listeners: {
79588 hide: function () {
79589 Ext.Viewport.remove(this);
79590 }
79591 }
79592 }).show();
79593 }
79594 }
79595 });
79596
79597 /**
79598 * @class Ext.chart.grid.HorizontalGrid
79599 * @extends Ext.draw.sprite.Sprite
79600 *
79601 * Horizontal Grid sprite. Used in Cartesian Charts.
79602 */
79603 Ext.define("Ext.chart.grid.HorizontalGrid", {
79604 extend: Ext.draw.sprite.Sprite ,
79605 alias: 'grid.horizontal',
79606
79607 inheritableStatics: {
79608 def: {
79609 processors: {
79610 x: 'number',
79611 y: 'number',
79612 width: 'number',
79613 height: 'number'
79614 },
79615
79616 defaults: {
79617 x: 0,
79618 y: 0,
79619 width: 1,
79620 height: 1,
79621 strokeStyle: '#DDD'
79622 }
79623 }
79624 },
79625
79626 render: function (surface, ctx, clipRegion) {
79627 var attr = this.attr,
79628 y = surface.roundPixel(attr.y),
79629 halfLineWidth = ctx.lineWidth * 0.5;
79630 ctx.beginPath();
79631 ctx.rect(clipRegion[0] - surface.matrix.getDX(), y + halfLineWidth, +clipRegion[2], attr.height);
79632 ctx.fill();
79633
79634 ctx.beginPath();
79635 ctx.moveTo(clipRegion[0] - surface.matrix.getDX(), y + halfLineWidth);
79636 ctx.lineTo(clipRegion[0] + clipRegion[2] - surface.matrix.getDX(), y + halfLineWidth);
79637 ctx.stroke();
79638 }
79639 });
79640
79641 /**
79642 * @class Ext.chart.grid.VerticalGrid
79643 * @extends Ext.draw.sprite.Sprite
79644 *
79645 * Vertical Grid sprite. Used in Cartesian Charts.
79646 */
79647 Ext.define("Ext.chart.grid.VerticalGrid", {
79648 extend: Ext.draw.sprite.Sprite ,
79649 alias: 'grid.vertical',
79650
79651 inheritableStatics: {
79652 def: {
79653 processors: {
79654 x: 'number',
79655 y: 'number',
79656 width: 'number',
79657 height: 'number'
79658 },
79659
79660 defaults: {
79661 x: 0,
79662 y: 0,
79663 width: 1,
79664 height: 1,
79665 strokeStyle: '#DDD'
79666 }
79667 }
79668 },
79669
79670 render: function (surface, ctx, clipRegion) {
79671 var attr = this.attr,
79672 x = surface.roundPixel(attr.x),
79673 halfLineWidth = ctx.lineWidth * 0.5;
79674 ctx.beginPath();
79675 ctx.rect(x - halfLineWidth, clipRegion[1] - surface.matrix.getDY(), attr.width, clipRegion[3]);
79676 ctx.fill();
79677
79678 ctx.beginPath();
79679 ctx.moveTo(x - halfLineWidth, clipRegion[1] - surface.matrix.getDY());
79680 ctx.lineTo(x - halfLineWidth, clipRegion[1] + clipRegion[3] - surface.matrix.getDY());
79681 ctx.stroke();
79682 }
79683 });
79684
79685 /**
79686 * @class Ext.chart.CartesianChart
79687 * @extends Ext.chart.AbstractChart
79688 *
79689 * Represents a chart that uses cartesian coordinates.
79690 * A cartesian chart have two directions, X direction and Y direction.
79691 * The series and axes are coordinated along these directions.
79692 * By default the x direction is horizontal and y direction is vertical,
79693 * You can swap the by setting {@link #flipXY} config to `true`.
79694 *
79695 * Cartesian series often treats x direction an y direction differently.
79696 * In most cases, data on x direction are assumed to be monotonically increasing.
79697 * Based on this property, cartesian series can be trimmed and summarized properly
79698 * to gain a better performance.
79699 *
79700 * @xtype chart
79701 */
79702
79703 Ext.define('Ext.chart.CartesianChart', {
79704 extend: Ext.chart.AbstractChart ,
79705 alternateClassName: 'Ext.chart.Chart',
79706
79707 config: {
79708 /**
79709 * @cfg {Boolean} flipXY Flip the direction of X and Y axis.
79710 * If flipXY is true, the X axes will be vertical and Y axes will be horizontal.
79711 */
79712 flipXY: false,
79713
79714 innerRegion: [0, 0, 1, 1]
79715 },
79716 xtype: 'chart',
79717 alias: 'Ext.chart.Chart',
79718
79719 getDirectionForAxis: function (position) {
79720 var flipXY = this.getFlipXY();
79721 if (position === 'left' || position === 'right') {
79722 if (flipXY) {
79723 return 'X';
79724 } else {
79725 return 'Y';
79726 }
79727 } else {
79728 if (flipXY) {
79729 return 'Y';
79730 } else {
79731 return 'X';
79732 }
79733 }
79734 },
79735
79736 /**
79737 * Layout the axes and series.
79738 */
79739 performLayout: function () {
79740 try {
79741 this.resizing++;
79742 this.callSuper();
79743 this.suspendThicknessChanged();
79744 var me = this,
79745 axes = me.getAxes(), axis,
79746 seriesList = me.getSeries(), series,
79747 axisSurface, thickness,
79748 size = me.element.getSize(),
79749 width = size.width,
79750 height = size.height,
79751 insetPadding = me.getInsetPadding(),
79752 innerPadding = me.getInnerPadding(),
79753 surface,
79754 shrinkBox = {
79755 top: insetPadding.top,
79756 left: insetPadding.left,
79757 right: insetPadding.right,
79758 bottom: insetPadding.bottom
79759 },
79760 gridSurface,
79761 mainRegion, innerWidth, innerHeight,
79762 elements, floating, matrix, i, ln,
79763 flipXY = me.getFlipXY();
79764
79765 if (width <= 0 || height <= 0) {
79766 return;
79767 }
79768
79769 for (i = 0; i < axes.length; i++) {
79770 axis = axes[i];
79771 axisSurface = axis.getSurface();
79772 floating = axis.getStyle && axis.getStyle() && axis.getStyle().floating;
79773 thickness = axis.getThickness();
79774 switch (axis.getPosition()) {
79775 case 'top':
79776 axisSurface.setRegion([0, shrinkBox.top, width, thickness]);
79777 break;
79778 case 'bottom':
79779 axisSurface.setRegion([0, height - (shrinkBox.bottom + thickness), width, thickness]);
79780 break;
79781 case 'left':
79782 axisSurface.setRegion([shrinkBox.left, 0, thickness, height]);
79783 break;
79784 case 'right':
79785 axisSurface.setRegion([width - (shrinkBox.right + thickness), 0, thickness, height]);
79786 break;
79787 }
79788 if (!floating) {
79789 shrinkBox[axis.getPosition()] += thickness;
79790 }
79791 }
79792
79793 width -= shrinkBox.left + shrinkBox.right;
79794 height -= shrinkBox.top + shrinkBox.bottom;
79795
79796 mainRegion = [shrinkBox.left, shrinkBox.top, width, height];
79797
79798 shrinkBox.left += innerPadding.left;
79799 shrinkBox.top += innerPadding.top;
79800 shrinkBox.right += innerPadding.right;
79801 shrinkBox.bottom += innerPadding.bottom;
79802
79803 innerWidth = width - innerPadding.left - innerPadding.right;
79804 innerHeight = height - innerPadding.top - innerPadding.bottom;
79805
79806 me.setInnerRegion([shrinkBox.left, shrinkBox.top, innerWidth, innerHeight]);
79807
79808 if (innerWidth <= 0 || innerHeight <= 0) {
79809 return;
79810 }
79811
79812 me.setMainRegion(mainRegion);
79813 me.getSurface('main').setRegion(mainRegion);
79814
79815 for (i = 0, ln = me.surfaceMap.grid && me.surfaceMap.grid.length; i < ln; i++) {
79816 gridSurface = me.surfaceMap.grid[i];
79817 gridSurface.setRegion(mainRegion);
79818 gridSurface.matrix.set(1, 0, 0, 1, innerPadding.left, innerPadding.top);
79819 gridSurface.matrix.inverse(gridSurface.inverseMatrix);
79820 }
79821
79822 for (i = 0; i < axes.length; i++) {
79823 axis = axes[i];
79824 axisSurface = axis.getSurface();
79825 matrix = axisSurface.matrix;
79826 elements = matrix.elements;
79827 switch (axis.getPosition()) {
79828 case 'top':
79829 case 'bottom':
79830 elements[4] = shrinkBox.left;
79831 axis.setLength(innerWidth);
79832 break;
79833 case 'left':
79834 case 'right':
79835 elements[5] = shrinkBox.top;
79836 axis.setLength(innerHeight);
79837 break;
79838 }
79839 axis.updateTitleSprite();
79840 matrix.inverse(axisSurface.inverseMatrix);
79841 }
79842
79843 for (i = 0, ln = seriesList.length; i < ln; i++) {
79844 series = seriesList[i];
79845 surface = series.getSurface();
79846 surface.setRegion(mainRegion);
79847 if (flipXY) {
79848 surface.matrix.set(0, -1, 1, 0, innerPadding.left, innerHeight + innerPadding.top);
79849 } else {
79850 surface.matrix.set(1, 0, 0, -1, innerPadding.left, innerHeight + innerPadding.top);
79851 }
79852 surface.matrix.inverse(surface.inverseMatrix);
79853 series.getOverlaySurface().setRegion(mainRegion);
79854 }
79855 me.redraw();
79856 me.onPlaceWatermark();
79857 } finally {
79858 this.resizing--;
79859 this.resumeThicknessChanged();
79860 }
79861 },
79862
79863 redraw: function () {
79864 var me = this,
79865 series = me.getSeries(),
79866 axes = me.getAxes(),
79867 region = me.getMainRegion(),
79868 innerWidth, innerHeight,
79869 innerPadding = me.getInnerPadding(),
79870 left, right, top, bottom, i, j,
79871 sprites, xRange, yRange, isSide, attr,
79872 axisX, axisY, range, visibleRange,
79873 flipXY = me.getFlipXY(),
79874 sprite, zIndex, zBase = 1000,
79875 markers, markerCount, markerIndex, markerSprite, markerZIndex;
79876
79877 if (!region) {
79878 return;
79879 }
79880
79881 innerWidth = region[2] - innerPadding.left - innerPadding.right;
79882 innerHeight = region[3] - innerPadding.top - innerPadding.bottom;
79883 for (i = 0; i < series.length; i++) {
79884 if ((axisX = series[i].getXAxis())) {
79885 visibleRange = axisX.getVisibleRange();
79886 xRange = axisX.getRange();
79887 xRange = [xRange[0] + (xRange[1] - xRange[0]) * visibleRange[0], xRange[0] + (xRange[1] - xRange[0]) * visibleRange[1]];
79888 } else {
79889 xRange = series[i].getXRange();
79890 }
79891
79892 if ((axisY = series[i].getYAxis())) {
79893 visibleRange = axisY.getVisibleRange();
79894 yRange = axisY.getRange();
79895 yRange = [yRange[0] + (yRange[1] - yRange[0]) * visibleRange[0], yRange[0] + (yRange[1] - yRange[0]) * visibleRange[1]];
79896 } else {
79897 yRange = series[i].getYRange();
79898 }
79899
79900 left = xRange[0];
79901 right = xRange[1];
79902 top = yRange[0];
79903 bottom = yRange[1];
79904
79905 attr = {
79906 visibleMinX: xRange[0],
79907 visibleMaxX: xRange[1],
79908 visibleMinY: yRange[0],
79909 visibleMaxY: yRange[1],
79910 innerWidth: innerWidth,
79911 innerHeight: innerHeight,
79912 flipXY: flipXY
79913 };
79914
79915 sprites = series[i].getSprites();
79916 for (j = 0; j < sprites.length; j++) {
79917
79918 // All the series now share the same surface, so we must assign
79919 // the sprites a zIndex that depends on the index of their series.
79920 sprite = sprites[j];
79921 zIndex = (sprite.attr.zIndex || 0);
79922 if (zIndex < zBase) {
79923 // Set the sprite's zIndex
79924 zIndex += (i+1) * 100 + zBase;
79925 sprite.attr.zIndex = zIndex;
79926 // Iterate through its marker sprites to do the same.
79927 markers = sprite.boundMarkers;
79928 if (markers) {
79929 markerCount = (markers.items ? markers.items.length : 0);
79930 if (markerCount) {
79931 for (markerIndex = 0; markerIndex < markerCount; markerIndex++) {
79932 markerSprite = markers.items[markerIndex];
79933 markerZIndex = (markerSprite.attr.zIndex || 0);
79934 if (markerZIndex == Number.MAX_VALUE) {
79935 markerSprite.attr.zIndex = zIndex;
79936 } else {
79937 if (markerZIndex < zBase) {
79938 markerSprite.attr.zIndex = zIndex + markerZIndex;
79939 }
79940 }
79941 }
79942 }
79943 }
79944 }
79945
79946 sprite.setAttributes(attr, true);
79947 }
79948 }
79949
79950 for (i = 0; i < axes.length; i++) {
79951 isSide = axes[i].isSide();
79952 sprites = axes[i].getSprites();
79953 range = axes[i].getRange();
79954 visibleRange = axes[i].getVisibleRange();
79955 attr = {
79956 dataMin: range[0],
79957 dataMax: range[1],
79958 visibleMin: visibleRange[0],
79959 visibleMax: visibleRange[1]
79960 };
79961 if (isSide) {
79962 attr.length = innerHeight;
79963 attr.startGap = innerPadding.bottom;
79964 attr.endGap = innerPadding.top;
79965 } else {
79966 attr.length = innerWidth;
79967 attr.startGap = innerPadding.left;
79968 attr.endGap = innerPadding.right;
79969 }
79970 for (j = 0; j < sprites.length; j++) {
79971 sprites[j].setAttributes(attr, true);
79972 }
79973 }
79974 me.renderFrame();
79975 me.callSuper(arguments);
79976 },
79977
79978 onPlaceWatermark: function () {
79979 var region0 = this.element.getBox(),
79980 region = this.getSurface ? this.getSurface('main').getRegion() : this.getItems().get(0).getRegion();
79981 if (region) {
79982 this.watermarkElement.setStyle({
79983 right: Math.round(region0.width - (region[2] + region[0])) + 'px',
79984 bottom: Math.round(region0.height - (region[3] + region[1])) + 'px'
79985 });
79986 }
79987 }
79988 });
79989
79990 /**
79991 * @class Ext.chart.grid.CircularGrid
79992 * @extends Ext.draw.sprite.Circle
79993 *
79994 * Circular Grid sprite. Used by Radar chart to render a series of concentric circles.
79995 */
79996 Ext.define('Ext.chart.grid.CircularGrid', {
79997 extend: Ext.draw.sprite.Circle ,
79998 alias: 'grid.circular',
79999
80000 inheritableStatics: {
80001 def: {
80002 defaults: {
80003 r: 1,
80004 strokeStyle: '#DDD'
80005 }
80006 }
80007 }
80008 });
80009
80010 /**
80011 * @class Ext.chart.grid.RadialGrid
80012 * @extends Ext.draw.sprite.Path
80013 *
80014 * Radial Grid sprite. Used by Radar chart to render a series of radial lines.
80015 * Represents the scale of the radar chart on the yField.
80016 */
80017 Ext.define('Ext.chart.grid.RadialGrid', {
80018 extend: Ext.draw.sprite.Path ,
80019 alias: 'grid.radial',
80020
80021 inheritableStatics: {
80022 def: {
80023 processors: {
80024 startRadius: 'number',
80025 endRadius: 'number'
80026 },
80027
80028 defaults: {
80029 startRadius: 0,
80030 endRadius: 1,
80031 scalingCenterX: 0,
80032 scalingCenterY: 0,
80033 strokeStyle: '#DDD'
80034 },
80035
80036 dirtyTriggers: {
80037 startRadius: 'path,bbox',
80038 endRadius: 'path,bbox'
80039 }
80040 }
80041 },
80042
80043 render: function () {
80044 this.callSuper(arguments);
80045 },
80046
80047 updatePath: function (path, attr) {
80048 var startRadius = attr.startRadius,
80049 endRadius = attr.endRadius;
80050 path.moveTo(startRadius, 0);
80051 path.lineTo(endRadius, 0);
80052 }
80053 });
80054
80055 /**
80056 * @class Ext.chart.PolarChart
80057 * @extends Ext.chart.AbstractChart
80058 *
80059 * Creates a chart that uses polar coordinates.
80060 */
80061 Ext.define('Ext.chart.PolarChart', {
80062
80063
80064
80065
80066
80067
80068 extend: Ext.chart.AbstractChart ,
80069 xtype: 'polar',
80070
80071 config: {
80072 /**
80073 * @cfg {Array} center Determines the center of the polar chart.
80074 * Updated when the chart performs layout.
80075 */
80076 center: [0, 0],
80077 /**
80078 * @cfg {Number} radius Determines the radius of the polar chart.
80079 * Updated when the chart performs layout.
80080 */
80081 radius: 0
80082 },
80083
80084 getDirectionForAxis: function (position) {
80085 if (position === 'radial') {
80086 return 'Y';
80087 } else {
80088 return 'X';
80089 }
80090 },
80091
80092 applyCenter: function (center, oldCenter) {
80093 if (oldCenter && center[0] === oldCenter[0] && center[1] === oldCenter[1]) {
80094 return;
80095 }
80096 return [+center[0], +center[1]];
80097 },
80098
80099 updateCenter: function (center) {
80100 var me = this,
80101 axes = me.getAxes(), axis,
80102 series = me.getSeries(), seriesItem,
80103 i, ln;
80104 for (i = 0, ln = axes.length; i < ln; i++) {
80105 axis = axes[i];
80106 axis.setCenter(center);
80107 }
80108
80109 for (i = 0, ln = series.length; i < ln; i++) {
80110 seriesItem = series[i];
80111 seriesItem.setCenter(center);
80112 }
80113 },
80114
80115 updateRadius: function (radius) {
80116 var me = this,
80117 axes = me.getAxes(), axis,
80118 series = me.getSeries(), seriesItem,
80119 i, ln;
80120 for (i = 0, ln = axes.length; i < ln; i++) {
80121 axis = axes[i];
80122 axis.setMinimum(0);
80123 axis.setLength(radius);
80124 axis.getSprites();
80125 }
80126
80127 for (i = 0, ln = series.length; i < ln; i++) {
80128 seriesItem = series[i];
80129 seriesItem.setRadius(radius);
80130 }
80131 },
80132
80133 doSetSurfaceRegion: function (surface, region) {
80134 var mainRegion = this.getMainRegion();
80135 surface.setRegion(region);
80136 surface.matrix.set(1, 0, 0, 1, mainRegion[0] - region[0], mainRegion[1] - region[1]);
80137 surface.inverseMatrix.set(1, 0, 0, 1, region[0] - mainRegion[0], region[1] - mainRegion[1]);
80138 },
80139
80140 performLayout: function () {
80141 try {
80142 this.resizing++;
80143 this.callSuper();
80144 var me = this,
80145 size = me.element.getSize(),
80146 fullRegion = [0, 0, size.width, size.height],
80147
80148 inset = me.getInsetPadding(),
80149 inner = me.getInnerPadding(),
80150
80151 left = inset.left,
80152 top = inset.top,
80153 width = size.width - left - inset.right,
80154 height = size.height - top - inset.bottom,
80155 region = [inset.left, inset.top, width, height],
80156
80157 innerWidth = width - inner.left - inner.right,
80158 innerHeight = height - inner.top - inner.bottom,
80159
80160 center = [innerWidth * 0.5 + inner.left, innerHeight * 0.5 + inner.top],
80161 radius = Math.min(innerWidth, innerHeight) * 0.5,
80162 axes = me.getAxes(), axis,
80163 series = me.getSeries(), seriesItem,
80164 i, ln;
80165
80166 me.setMainRegion(region);
80167
80168 for (i = 0, ln = series.length; i < ln; i++) {
80169 seriesItem = series[i];
80170 me.doSetSurfaceRegion(seriesItem.getSurface(), region);
80171 me.doSetSurfaceRegion(seriesItem.getOverlaySurface(), fullRegion);
80172 }
80173
80174 me.doSetSurfaceRegion(me.getSurface(), fullRegion);
80175 for (i = 0, ln = me.surfaceMap.grid && me.surfaceMap.grid.length; i < ln; i++) {
80176 me.doSetSurfaceRegion(me.surfaceMap.grid[i], fullRegion);
80177 }
80178 for (i = 0, ln = axes.length; i < ln; i++) {
80179 axis = axes[i];
80180 me.doSetSurfaceRegion(axis.getSurface(), fullRegion);
80181 }
80182
80183 me.setRadius(radius);
80184 me.setCenter(center);
80185 me.redraw();
80186 } finally {
80187 this.resizing--;
80188 }
80189 },
80190
80191 getEventXY: function (e) {
80192 e = (e.changedTouches && e.changedTouches[0]) || e.event || e.browserEvent || e;
80193 var me = this,
80194 xy = me.element.getXY(),
80195 padding = me.getInsetPadding();
80196 return [e.pageX - xy[0] - padding.left, e.pageY - xy[1] - padding.top];
80197 },
80198
80199 redraw: function () {
80200 var me = this,
80201 axes = me.getAxes(), axis,
80202 series = me.getSeries(), seriesItem,
80203 i, ln;
80204
80205 for (i = 0, ln = axes.length; i < ln; i++) {
80206 axis = axes[i];
80207 axis.getSprites();
80208 }
80209
80210 for (i = 0, ln = series.length; i < ln; i++) {
80211 seriesItem = series[i];
80212 seriesItem.getSprites();
80213 }
80214
80215 me.renderFrame();
80216 me.callSuper(arguments);
80217 }
80218 });
80219
80220 /**
80221 * @class Ext.chart.SpaceFillingChart
80222 * @extends Ext.chart.AbstractChart
80223 *
80224 * Creates a chart that fills the entire area of the chart.
80225 * e.g. Gauge Charts
80226 */
80227 Ext.define('Ext.chart.SpaceFillingChart', {
80228
80229 extend: Ext.chart.AbstractChart ,
80230 xtype: 'spacefilling',
80231
80232 config: {
80233
80234 },
80235
80236 performLayout: function () {
80237 try {
80238 this.resizing++;
80239 this.callSuper();
80240 var me = this,
80241 size = me.element.getSize(),
80242 series = me.getSeries(), seriesItem,
80243 padding = me.getInsetPadding(),
80244 width = size.width - padding.left - padding.right,
80245 height = size.height - padding.top - padding.bottom,
80246 region = [padding.left, padding.top, width, height],
80247 fullRegion = [0, 0, size.width, size.height],
80248 i, ln;
80249 me.getSurface().setRegion(region);
80250 me.setMainRegion(region);
80251 for (i = 0, ln = series.length; i < ln; i++) {
80252 seriesItem = series[i];
80253 seriesItem.getSurface().setRegion(region);
80254 seriesItem.setRegion(region);
80255
80256 seriesItem.getOverlaySurface().setRegion(fullRegion);
80257 }
80258 me.redraw();
80259 } finally {
80260 this.resizing--;
80261 }
80262 },
80263
80264 redraw: function () {
80265 var me = this,
80266 series = me.getSeries(), seriesItem,
80267 i, ln;
80268
80269 for (i = 0, ln = series.length; i < ln; i++) {
80270 seriesItem = series[i];
80271 seriesItem.getSprites();
80272 }
80273
80274 me.renderFrame();
80275 me.callSuper(arguments);
80276 }
80277 });
80278
80279 /**
80280 * @class Ext.chart.axis.Category
80281 * @extends Ext.chart.axis.Axis
80282 *
80283 * A type of axis that displays items in categories. This axis is generally used to
80284 * display categorical information like names of items, month names, quarters, etc.
80285 * but no quantitative values. For that other type of information {@link Ext.chart.axis.Numeric Numeric}
80286 * axis are more suitable.
80287 *
80288 * As with other axis you can set the position of the axis and its title. For example:
80289 *
80290 * @example preview
80291 * var chart = new Ext.chart.CartesianChart({
80292 * animate: true,
80293 * innerPadding: {
80294 * left: 40,
80295 * right: 40
80296 * },
80297 * store: {
80298 * fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
80299 * data: [
80300 * {'name':'metric one', 'data1':10, 'data2':12, 'data3':14, 'data4':8, 'data5':13},
80301 * {'name':'metric two', 'data1':7, 'data2':8, 'data3':16, 'data4':10, 'data5':3},
80302 * {'name':'metric three', 'data1':5, 'data2':2, 'data3':14, 'data4':12, 'data5':7},
80303 * {'name':'metric four', 'data1':2, 'data2':14, 'data3':6, 'data4':1, 'data5':23},
80304 * {'name':'metric five', 'data1':27, 'data2':38, 'data3':36, 'data4':13, 'data5':33}
80305 * ]
80306 * },
80307 * axes: [{
80308 * type: 'category',
80309 * position: 'bottom',
80310 * fields: ['name'],
80311 * title: {
80312 * text: 'Sample Values',
80313 * fontSize: 15
80314 * }
80315 * }],
80316 * series: [{
80317 * type: 'area',
80318 * subStyle: {
80319 * fill: ['blue', 'green', 'red']
80320 * },
80321 * xField: 'name',
80322 * yField: ['data1', 'data2', 'data3']
80323 *
80324 * }]
80325 * });
80326 * Ext.Viewport.setLayout('fit');
80327 * Ext.Viewport.add(chart);
80328 *
80329 * In this example with set the category axis to the bottom of the surface, bound the axis to
80330 * the `name` property and set as title "Sample Values".
80331 */
80332
80333 Ext.define('Ext.chart.axis.Category', {
80334
80335
80336
80337
80338 extend: Ext.chart.axis.Axis ,
80339 alias: 'axis.category',
80340 type: 'category',
80341
80342 config: {
80343 layout: 'combineDuplicate',
80344
80345 segmenter: 'names'
80346 }
80347 });
80348
80349 /**
80350 * @class Ext.chart.axis.Numeric
80351 * @extends Ext.chart.axis.Axis
80352 *
80353 * An axis to handle numeric values. This axis is used for quantitative data as
80354 * opposed to the category axis. You can set minimum and maximum values to the
80355 * axis so that the values are bound to that. If no values are set, then the
80356 * scale will auto-adjust to the values.
80357 *
80358 * @example preview
80359 * var chart = new Ext.chart.CartesianChart({
80360 * animate: true,
80361 * store: {
80362 * fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
80363 * data: [
80364 * {'name':1, 'data1':10, 'data2':12, 'data3':14, 'data4':8, 'data5':13},
80365 * {'name':2, 'data1':7, 'data2':8, 'data3':16, 'data4':10, 'data5':3},
80366 * {'name':3, 'data1':5, 'data2':2, 'data3':14, 'data4':12, 'data5':7},
80367 * {'name':4, 'data1':2, 'data2':14, 'data3':6, 'data4':1, 'data5':23},
80368 * {'name':5, 'data1':27, 'data2':38, 'data3':36, 'data4':13, 'data5':33}
80369 * ]
80370 * },
80371 * axes: [{
80372 * type: 'numeric',
80373 * position: 'left',
80374 * fields: ['data1', 'data2', 'data3', 'data4', 'data5'],
80375 * title: 'Sample Values',
80376 * grid: {
80377 * odd: {
80378 * opacity: 1,
80379 * fill: '#ddd',
80380 * stroke: '#bbb',
80381 * 'lineWidth': 1
80382 * }
80383 * },
80384 * minimum: 0,
80385 * adjustMinimumByMajorUnit: 0
80386 * }],
80387 * series: [{
80388 * type: 'area',
80389 * subStyle: {
80390 * fill: ['blue', 'green', 'red']
80391 * },
80392 * xField: 'name',
80393 * yField: ['data1', 'data2', 'data3']
80394 *
80395 * }]
80396 * });
80397 * Ext.Viewport.setLayout('fit');
80398 * Ext.Viewport.add(chart);
80399 * In this example we create an axis of Numeric type. We set a minimum value so that
80400 * even if all series have values greater than zero, the grid starts at zero. We bind
80401 * the axis onto the left part of the surface by setting _position_ to _left_.
80402 * We bind three different store fields to this axis by setting _fields_ to an array.
80403 * We set the title of the axis to _Number of Hits_ by using the _title_ property.
80404 * We use a _grid_ configuration to set odd background rows to a certain style and even rows
80405 * to be transparent/ignored.
80406 *
80407 */
80408 Ext.define('Ext.chart.axis.Numeric', {
80409 extend: Ext.chart.axis.Axis ,
80410 alias: 'axis.numeric',
80411 type: 'numeric',
80412
80413 config: {
80414 layout: 'continuous',
80415
80416 segmenter: 'numeric',
80417
80418 aggregator: 'double'
80419 }
80420 });
80421
80422 /**
80423 * @class Ext.chart.axis.Time
80424 * @extends Ext.chart.axis.Numeric
80425 *
80426 * A type of axis whose units are measured in time values. Use this axis
80427 * for listing dates that you will want to group or dynamically change.
80428 * If you just want to display dates as categories then use the
80429 * Category class for axis instead.
80430 *
80431 * @example preview
80432 * var chart = new Ext.chart.CartesianChart({
80433 * animate: true,
80434 * store: {
80435 * fields: ['time', 'open', 'high', 'low', 'close'],
80436 * data: [
80437 * {'time':new Date('Jan 1 2010').getTime(), 'open':600, 'high':614, 'low':578, 'close':590},
80438 * {'time':new Date('Jan 2 2010').getTime(), 'open':590, 'high':609, 'low':580, 'close':580},
80439 * {'time':new Date('Jan 3 2010').getTime(), 'open':580, 'high':602, 'low':578, 'close':602},
80440 * {'time':new Date('Jan 4 2010').getTime(), 'open':602, 'high':614, 'low':586, 'close':586},
80441 * {'time':new Date('Jan 5 2010').getTime(), 'open':586, 'high':602, 'low':565, 'close':565}
80442 * ]
80443 * },
80444 * axes: [{
80445 * type: 'numeric',
80446 * position: 'left',
80447 * fields: ['open', 'high', 'low', 'close'],
80448 * title: {
80449 * text: 'Sample Values',
80450 * fontSize: 15
80451 * },
80452 * grid: true,
80453 * minimum: 560,
80454 * maximum: 640
80455 * }, {
80456 * type: 'time',
80457 * position: 'bottom',
80458 * fields: ['time'],
80459 * fromDate: new Date('Dec 31 2009'),
80460 * toDate: new Date('Jan 6 2010'),
80461 * title: {
80462 * text: 'Sample Values',
80463 * fontSize: 15
80464 * },
80465 * style: {
80466 * axisLine: false
80467 * }
80468 * }],
80469 * series: [{
80470 * type: 'candlestick',
80471 * xField: 'time',
80472 * openField: 'open',
80473 * highField: 'high',
80474 * lowField: 'low',
80475 * closeField: 'close',
80476 * style: {
80477 * ohlcType: 'ohlc',
80478 * dropStyle: {
80479 * fill: 'rgb(237, 123, 43)',
80480 * stroke: 'rgb(237, 123, 43)'
80481 * },
80482 * raiseStyle: {
80483 * fill: 'rgb(55, 153, 19)',
80484 * stroke: 'rgb(55, 153, 19)'
80485 * }
80486 * },
80487 * aggregator: {
80488 * strategy: 'time'
80489 * }
80490 * }]
80491 * });
80492 * Ext.Viewport.setLayout('fit');
80493 * Ext.Viewport.add(chart);
80494 */
80495 Ext.define('Ext.chart.axis.Time', {
80496 extend: Ext.chart.axis.Numeric ,
80497 alias: 'axis.time',
80498 type: 'time',
80499
80500 config: {
80501 /**
80502 * @cfg {Boolean} calculateByLabelSize
80503 * The minimum value drawn by the axis. If not set explicitly, the axis
80504 * minimum will be calculated automatically.
80505 */
80506 calculateByLabelSize: true,
80507
80508 /**
80509 * @cfg {String/Boolean} dateFormat
80510 * Indicates the format the date will be rendered on.
80511 * For example: 'M d' will render the dates as 'Jan 30', etc.
80512 */
80513 dateFormat: null,
80514
80515 /**
80516 * @cfg {Date} fromDate The starting date for the time axis.
80517 */
80518 fromDate: null,
80519
80520 /**
80521 * @cfg {Date} toDate The ending date for the time axis.
80522 */
80523 toDate: null,
80524
80525 /**
80526 * @cfg {Array} [step=[Ext.Date.DAY, 1]] An array with two components:
80527 *
80528 * - The unit of the step (Ext.Date.DAY, Ext.Date.MONTH, etc).
80529 * - The number of units for the step (1, 2, etc).
80530 *
80531 */
80532 step: [Ext.Date.DAY, 1],
80533
80534 layout: 'continuous',
80535
80536 segmenter: 'time',
80537
80538 aggregator: 'time'
80539 },
80540
80541 updateDateFormat: function (format) {
80542 this.setRenderer(function (date) {
80543 return Ext.Date.format(new Date(date), format);
80544 });
80545 },
80546
80547 updateFromDate: function (date) {
80548 this.setMinimum(+date);
80549 },
80550
80551 updateToDate: function (date) {
80552 this.setMaximum(+date);
80553 },
80554
80555 getCoordFor: function (value) {
80556 if (Ext.isString(value)) {
80557 value = new Date(value);
80558 }
80559 return +value;
80560 }
80561 });
80562
80563 /**
80564 * @class Ext.chart.interactions.CrossZoom
80565 * @extends Ext.chart.interactions.Abstract
80566 *
80567 * The CrossZoom interaction allows the user to zoom in on a selected area of the chart.
80568 *
80569 * @example preview
80570 * var lineChart = new Ext.chart.CartesianChart({
80571 * interactions: [{
80572 * type: 'crosszoom'
80573 * }],
80574 * animate: true,
80575 * store: {
80576 * fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
80577 * data: [
80578 * {'name':'metric one', 'data1':10, 'data2':12, 'data3':14, 'data4':8, 'data5':13},
80579 * {'name':'metric two', 'data1':7, 'data2':8, 'data3':16, 'data4':10, 'data5':3},
80580 * {'name':'metric three', 'data1':5, 'data2':2, 'data3':14, 'data4':12, 'data5':7},
80581 * {'name':'metric four', 'data1':2, 'data2':14, 'data3':6, 'data4':1, 'data5':23},
80582 * {'name':'metric five', 'data1':27, 'data2':38, 'data3':36, 'data4':13, 'data5':33}
80583 * ]
80584 * },
80585 * axes: [{
80586 * type: 'numeric',
80587 * position: 'left',
80588 * fields: ['data1'],
80589 * title: {
80590 * text: 'Sample Values',
80591 * fontSize: 15
80592 * },
80593 * grid: true,
80594 * minimum: 0
80595 * }, {
80596 * type: 'category',
80597 * position: 'bottom',
80598 * fields: ['name'],
80599 * title: {
80600 * text: 'Sample Values',
80601 * fontSize: 15
80602 * }
80603 * }],
80604 * series: [{
80605 * type: 'line',
80606 * highlight: {
80607 * size: 7,
80608 * radius: 7
80609 * },
80610 * style: {
80611 * stroke: 'rgb(143,203,203)'
80612 * },
80613 * xField: 'name',
80614 * yField: 'data1',
80615 * marker: {
80616 * type: 'path',
80617 * path: ['M', -2, 0, 0, 2, 2, 0, 0, -2, 'Z'],
80618 * stroke: 'blue',
80619 * lineWidth: 0
80620 * }
80621 * }, {
80622 * type: 'line',
80623 * highlight: {
80624 * size: 7,
80625 * radius: 7
80626 * },
80627 * fill: true,
80628 * xField: 'name',
80629 * yField: 'data3',
80630 * marker: {
80631 * type: 'circle',
80632 * radius: 4,
80633 * lineWidth: 0
80634 * }
80635 * }]
80636 * });
80637 * Ext.Viewport.setLayout('fit');
80638 * Ext.Viewport.add(lineChart);
80639 */
80640 Ext.define('Ext.chart.interactions.CrossZoom', {
80641
80642 extend: Ext.chart.interactions.Abstract ,
80643
80644 type: 'crosszoom',
80645 alias: 'interaction.crosszoom',
80646
80647 config: {
80648 /**
80649 * @cfg {Object/Array} axes
80650 * Specifies which axes should be made navigable. The config value can take the following formats:
80651 *
80652 * - An Object whose keys correspond to the {@link Ext.chart.axis.Axis#position position} of each
80653 * axis that should be made navigable. Each key's value can either be an Object with further
80654 * configuration options for each axis or simply `true` for a default set of options.
80655 * {
80656 * type: 'crosszoom',
80657 * axes: {
80658 * left: {
80659 * maxZoom: 5,
80660 * allowPan: false
80661 * },
80662 * bottom: true
80663 * }
80664 * }
80665 *
80666 * If using the full Object form, the following options can be specified for each axis:
80667 *
80668 * - minZoom (Number) A minimum zoom level for the axis. Defaults to `1` which is its natural size.
80669 * - maxZoom (Number) A maximum zoom level for the axis. Defaults to `10`.
80670 * - startZoom (Number) A starting zoom level for the axis. Defaults to `1`.
80671 * - allowZoom (Boolean) Whether zooming is allowed for the axis. Defaults to `true`.
80672 * - allowPan (Boolean) Whether panning is allowed for the axis. Defaults to `true`.
80673 * - startPan (Boolean) A starting panning offset for the axis. Defaults to `0`.
80674 *
80675 * - An Array of strings, each one corresponding to the {@link Ext.chart.axis.Axis#position position}
80676 * of an axis that should be made navigable. The default options will be used for each named axis.
80677 *
80678 * {
80679 * type: 'crosszoom',
80680 * axes: ['left', 'bottom']
80681 * }
80682 *
80683 * If the `axes` config is not specified, it will default to making all axes navigable with the
80684 * default axis options.
80685 */
80686 axes: true,
80687
80688 gesture: 'drag',
80689
80690 undoButton: {}
80691 },
80692
80693 stopAnimationBeforeSync: false,
80694
80695 zoomAnimationInProgress: false,
80696
80697 constructor: function () {
80698 this.callSuper(arguments);
80699 this.zoomHistory = [];
80700 },
80701
80702 applyAxes: function (axesConfig) {
80703 var result = {};
80704 if (axesConfig === true) {
80705 return {
80706 top: {},
80707 right: {},
80708 bottom: {},
80709 left: {}
80710 };
80711 } else if (Ext.isArray(axesConfig)) {
80712 // array of axis names - translate to full object form
80713 result = {};
80714 Ext.each(axesConfig, function (axis) {
80715 result[axis] = {};
80716 });
80717 } else if (Ext.isObject(axesConfig)) {
80718 Ext.iterate(axesConfig, function (key, val) {
80719 // axis name with `true` value -> translate to object
80720 if (val === true) {
80721 result[key] = {};
80722 } else if (val !== false) {
80723 result[key] = val;
80724 }
80725 });
80726 }
80727 return result;
80728 },
80729
80730 applyUndoButton: function (button, oldButton) {
80731 var me = this;
80732 if (button) {
80733 if (oldButton) {
80734 oldButton.destroy();
80735 }
80736 return Ext.create('Ext.Button', Ext.apply({
80737 cls: [],
80738 iconCls: 'refresh',
80739 text: 'Undo Zoom',
80740 disabled: true,
80741 handler: function () {
80742 me.undoZoom();
80743 }
80744 }, button));
80745 } else if (oldButton) {
80746 oldButton.destroy();
80747 }
80748 },
80749
80750 getGestures: function () {
80751 var me = this,
80752 gestures = {};
80753 gestures[me.getGesture()] = 'onGesture';
80754 gestures[me.getGesture() + 'start'] = 'onGestureStart';
80755 gestures[me.getGesture() + 'end'] = 'onGestureEnd';
80756 gestures.doubletap = 'onDoubleTap';
80757 return gestures;
80758 },
80759
80760 getSurface: function () {
80761 return this.getChart() && this.getChart().getSurface('main');
80762 },
80763
80764 setSeriesOpacity: function (opacity) {
80765 var surface = this.getChart() && this.getChart().getSurface('series-surface', 'series');
80766 if (surface) {
80767 surface.element.setStyle('opacity', opacity);
80768 }
80769 },
80770
80771 onGestureStart: function (e) {
80772 var me = this,
80773 chart = me.getChart(),
80774 surface = me.getSurface(),
80775 region = chart.getInnerRegion(),
80776 chartWidth = region[2],
80777 chartHeight = region[3],
80778 xy = chart.element.getXY(),
80779 x = e.pageX - xy[0] - region[0],
80780 y = e.pageY - xy[1] - region[1];
80781
80782 if (me.zoomAnimationInProgress) {
80783 return;
80784 }
80785 if (x > 0 && x < chartWidth && y > 0 && y < chartHeight) {
80786 me.lockEvents(me.getGesture());
80787 me.startX = x;
80788 me.startY = y;
80789 me.selectionRect = surface.add({
80790 type: 'rect',
80791 globalAlpha: 0.5,
80792 fillStyle: 'rgba(80,80,140,0.5)',
80793 strokeStyle: 'rgba(80,80,140,1)',
80794 lineWidth: 2,
80795 x: x,
80796 y: y,
80797 width: 0,
80798 height: 0,
80799 zIndex: 10000
80800 });
80801 me.setSeriesOpacity(0.8);
80802 return false;
80803 }
80804 },
80805
80806 onGesture: function (e) {
80807 var me = this;
80808 if (me.zoomAnimationInProgress) {
80809 return;
80810 }
80811 if (me.getLocks()[me.getGesture()] === me) {
80812 var chart = me.getChart(),
80813 surface = me.getSurface(),
80814 region = chart.getInnerRegion(),
80815 chartWidth = region[2],
80816 chartHeight = region[3],
80817 xy = chart.element.getXY(),
80818 x = e.pageX - xy[0] - region[0],
80819 y = e.pageY - xy[1] - region[1];
80820
80821 if (x < 0) {
80822 x = 0;
80823 } else if (x > chartWidth) {
80824 x = chartWidth;
80825 }
80826 if (y < 0) {
80827 y = 0;
80828 } else if (y > chartHeight) {
80829 y = chartHeight;
80830 }
80831 me.selectionRect.setAttributes({
80832 width: x - me.startX,
80833 height: y - me.startY
80834 });
80835 if (Math.abs(me.startX - x) < 11 || Math.abs(me.startY - y) < 11) {
80836 me.selectionRect.setAttributes({globalAlpha: 0.5});
80837 } else {
80838 me.selectionRect.setAttributes({globalAlpha: 1});
80839 }
80840 surface.renderFrame();
80841 return false;
80842 }
80843 },
80844
80845 onGestureEnd: function (e) {
80846 var me = this;
80847 if (me.zoomAnimationInProgress) {
80848 return;
80849 }
80850 if (me.getLocks()[me.getGesture()] === me) {
80851 var chart = me.getChart(),
80852 surface = me.getSurface(),
80853 region = chart.getInnerRegion(),
80854 chartWidth = region[2],
80855 chartHeight = region[3],
80856 xy = chart.element.getXY(),
80857 x = e.pageX - xy[0] - region[0],
80858 y = e.pageY - xy[1] - region[1];
80859
80860 if (x < 0) {
80861 x = 0;
80862 } else if (x > chartWidth) {
80863 x = chartWidth;
80864 }
80865 if (y < 0) {
80866 y = 0;
80867 } else if (y > chartHeight) {
80868 y = chartHeight;
80869 }
80870 if (Math.abs(me.startX - x) < 11 || Math.abs(me.startY - y) < 11) {
80871 surface.remove(me.selectionRect);
80872 } else {
80873 me.zoomBy([
80874 Math.min(me.startX, x) / chartWidth,
80875 1 - Math.max(me.startY, y) / chartHeight,
80876 Math.max(me.startX, x) / chartWidth,
80877 1 - Math.min(me.startY, y) / chartHeight
80878 ]);
80879
80880 me.selectionRect.setAttributes({
80881 x: Math.min(me.startX, x),
80882 y: Math.min(me.startY, y),
80883 width: Math.abs(me.startX - x),
80884 height: Math.abs(me.startY - y)
80885 });
80886
80887 me.selectionRect.fx.setConfig(chart.getAnimate() || {duration: 0});
80888 me.selectionRect.setAttributes({
80889 globalAlpha: 0,
80890 x: 0,
80891 y: 0,
80892 width: chartWidth,
80893 height: chartHeight
80894 });
80895
80896 me.zoomAnimationInProgress = true;
80897
80898 chart.suspendThicknessChanged();
80899 me.selectionRect.fx.on('animationend', function () {
80900 chart.resumeThicknessChanged();
80901
80902 surface.remove(me.selectionRect);
80903 me.selectionRect = null;
80904
80905 me.zoomAnimationInProgress = false;
80906 });
80907 }
80908
80909 surface.renderFrame();
80910 me.sync();
80911 me.unlockEvents(me.getGesture());
80912 me.setSeriesOpacity(1.0);
80913
80914 if (!me.zoomAnimationInProgress) {
80915 surface.remove(me.selectionRect);
80916 me.selectionRect = null;
80917 }
80918 }
80919 },
80920
80921 zoomBy: function (region) {
80922 var me = this,
80923 axisConfigs = me.getAxes(),
80924 axes = me.getChart().getAxes(),
80925 config,
80926 zoomMap = {};
80927
80928 for (var i = 0; i < axes.length; i++) {
80929 var axis = axes[i];
80930 config = axisConfigs[axis.getPosition()];
80931 if (config && config.allowZoom !== false) {
80932 var isSide = axis.isSide(),
80933 oldRange = axis.getVisibleRange();
80934 zoomMap[axis.getId()] = oldRange.slice(0);
80935 if (!isSide) {
80936 axis.setVisibleRange([
80937 (oldRange[1] - oldRange[0]) * region[0] + oldRange[0],
80938 (oldRange[1] - oldRange[0]) * region[2] + oldRange[0]
80939 ]);
80940 } else {
80941 axis.setVisibleRange([
80942 (oldRange[1] - oldRange[0]) * region[1] + oldRange[0],
80943 (oldRange[1] - oldRange[0]) * region[3] + oldRange[0]
80944 ]);
80945 }
80946 }
80947 }
80948
80949 me.zoomHistory.push(zoomMap);
80950 me.getUndoButton().setDisabled(false);
80951 },
80952
80953 undoZoom: function () {
80954 var zoomMap = this.zoomHistory.pop(),
80955 axes = this.getChart().getAxes();
80956 if (zoomMap) {
80957 for (var i = 0; i < axes.length; i++) {
80958 var axis = axes[i];
80959 if (zoomMap[axis.getId()]) {
80960 axis.setVisibleRange(zoomMap[axis.getId()]);
80961 }
80962 }
80963 }
80964 this.getUndoButton().setDisabled(this.zoomHistory.length === 0);
80965 this.sync();
80966 },
80967
80968 onDoubleTap: function (e) {
80969 this.undoZoom();
80970 }
80971 });
80972
80973 /**
80974 * The Crosshair interaction allows the user to get precise values for a specific point on the chart.
80975 * The values are obtained by single-touch dragging on the chart.
80976 *
80977 * @example preview
80978 * var lineChart = Ext.create('Ext.chart.CartesianChart', {
80979 * innerPadding: 20,
80980 * interactions: [{
80981 * type: 'crosshair',
80982 * axes: {
80983 * left: {
80984 * label: {
80985 * fillStyle: 'white'
80986 * },
80987 * rect: {
80988 * fillStyle: 'brown',
80989 * radius: 6
80990 * }
80991 * },
80992 * bottom: {
80993 * label: {
80994 * fontSize: '14px',
80995 * fontWeight: 'bold'
80996 * }
80997 * }
80998 * },
80999 * lines: {
81000 * horizontal: {
81001 * strokeStyle: 'brown',
81002 * lineWidth: 2,
81003 * lineDash: [20, 2, 2, 2, 2, 2, 2, 2]
81004 * }
81005 * }
81006 * }],
81007 * store: {
81008 * fields: ['name', 'data'],
81009 * data: [
81010 * {name: 'apple', data: 300},
81011 * {name: 'orange', data: 900},
81012 * {name: 'banana', data: 800},
81013 * {name: 'pear', data: 400},
81014 * {name: 'grape', data: 500}
81015 * ]
81016 * },
81017 * axes: [{
81018 * type: 'numeric',
81019 * position: 'left',
81020 * fields: ['data'],
81021 * title: {
81022 * text: 'Value',
81023 * fontSize: 15
81024 * },
81025 * grid: true,
81026 * label: {
81027 * rotationRads: -Math.PI / 4
81028 * }
81029 * }, {
81030 * type: 'category',
81031 * position: 'bottom',
81032 * fields: ['name'],
81033 * title: {
81034 * text: 'Category',
81035 * fontSize: 15
81036 * }
81037 * }],
81038 * series: [{
81039 * type: 'line',
81040 * style: {
81041 * strokeStyle: 'black'
81042 * },
81043 * xField: 'name',
81044 * yField: 'data',
81045 * marker: {
81046 * type: 'circle',
81047 * radius: 5,
81048 * fillStyle: 'lightblue'
81049 * }
81050 * }]
81051 * });
81052 * Ext.Viewport.setLayout('fit');
81053 * Ext.Viewport.add(lineChart);
81054 */
81055
81056 Ext.define('Ext.chart.interactions.Crosshair', {
81057
81058 extend: Ext.chart.interactions.Abstract ,
81059
81060
81061
81062
81063
81064
81065
81066 type: 'crosshair',
81067 alias: 'interaction.crosshair',
81068
81069 config: {
81070 /**
81071 * @cfg {Object} axes
81072 * Specifies label text and label rect configs on per axis basis or as a single config for all axes.
81073 *
81074 * {
81075 * type: 'crosshair',
81076 * axes: {
81077 * label: { fillStyle: 'white' },
81078 * rect: { fillStyle: 'maroon'}
81079 * }
81080 * }
81081 *
81082 * In case per axis configuration is used, an object with keys corresponding
81083 * to the {@link Ext.chart.axis.Axis#position position} must be provided.
81084 *
81085 * {
81086 * type: 'crosshair',
81087 * axes: {
81088 * left: {
81089 * label: { fillStyle: 'white' },
81090 * rect: {
81091 * fillStyle: 'maroon',
81092 * radius: 4
81093 * }
81094 * },
81095 * bottom: {
81096 * label: {
81097 * fontSize: '14px',
81098 * fontWeight: 'bold'
81099 * },
81100 * rect: { fillStyle: 'white' }
81101 * }
81102 * }
81103 *
81104 * If the `axes` config is not specified, the following defaults will be used:
81105 * - `label` will use values from the {@link Ext.chart.axis.Axis#label label} config.
81106 * - `rect` will use the 'white' fillStyle.
81107 */
81108 axes: {
81109 top: {label: {}, rect: {}},
81110 right: {label: {}, rect: {}},
81111 bottom: {label: {}, rect: {}},
81112 left: {label: {}, rect: {}}
81113 },
81114
81115 /**
81116 * @cfg {Object} lines
81117 * Specifies attributes of horizontal and vertical lines that make up the crosshair.
81118 * If this config is missing, black dashed lines will be used.
81119 *
81120 * {
81121 * horizontal: {
81122 * strokeStyle: 'red',
81123 * lineDash: [] // solid line
81124 * },
81125 * vertical: {
81126 * lineWidth: 2,
81127 * lineDash: [15, 5, 5, 5]
81128 * }
81129 * }
81130 */
81131 lines: {
81132 horizontal: {
81133 strokeStyle: 'black',
81134 lineDash: [5, 5]
81135 },
81136 vertical: {
81137 strokeStyle: 'black',
81138 lineDash: [5, 5]
81139 }
81140 },
81141 gesture: 'drag'
81142 },
81143
81144 applyAxes: function (axesConfig, oldAxesConfig) {
81145 return Ext.merge(oldAxesConfig || {}, axesConfig);
81146 },
81147
81148 applyLines: function (linesConfig, oldLinesConfig) {
81149 return Ext.merge(oldLinesConfig || {}, linesConfig);
81150 },
81151
81152 updateChart: function (chart) {
81153 if (!(chart instanceof Ext.chart.CartesianChart)) {
81154 throw 'Crosshair interaction can only be used on cartesian charts.';
81155 }
81156 this.callParent(arguments);
81157 },
81158
81159 getGestures: function () {
81160 var me = this,
81161 gestures = {};
81162 gestures[me.getGesture()] = 'onGesture';
81163 gestures[me.getGesture() + 'start'] = 'onGestureStart';
81164 gestures[me.getGesture() + 'end'] = 'onGestureEnd';
81165 return gestures;
81166 },
81167
81168 onGestureStart: function (e) {
81169 var me = this,
81170 chart = me.getChart(),
81171 surface = chart.getSurface('overlay-surface'),
81172 region = chart.getInnerRegion(),
81173 chartWidth = region[2],
81174 chartHeight = region[3],
81175 xy = chart.element.getXY(),
81176 x = e.pageX - xy[0] - region[0],
81177 y = e.pageY - xy[1] - region[1],
81178 axes = chart.getAxes(),
81179 axesConfig = me.getAxes(),
81180 linesConfig = me.getLines(),
81181 axis, axisSurface, axisRegion, axisWidth, axisHeight, axisPosition,
81182 axisLabel, labelPadding,
81183 axisSprite, attr, axisThickness, lineWidth, halfLineWidth,
81184 i;
81185
81186 if (x > 0 && x < chartWidth && y > 0 && y < chartHeight) {
81187 me.lockEvents(me.getGesture());
81188 me.horizontalLine = surface.add(Ext.apply({
81189 xclass: 'Ext.chart.grid.HorizontalGrid',
81190 x: 0,
81191 y: y,
81192 width: chartWidth
81193 }, linesConfig.horizontal));
81194 me.verticalLine = surface.add(Ext.apply({
81195 xclass: 'Ext.chart.grid.VerticalGrid',
81196 x: x,
81197 y: 0,
81198 height: chartHeight
81199 }, linesConfig.vertical));
81200 me.axesLabels = me.axesLabels || {};
81201 for (i = 0; i < axes.length; i++) {
81202 axis = axes[i];
81203 axisSurface = axis.getSurface();
81204 axisRegion = axisSurface.getRegion();
81205 axisSprite = axis.getSprites()[0];
81206 axisWidth = axisRegion[2];
81207 axisHeight = axisRegion[3];
81208 axisPosition = axis.getPosition();
81209 attr = axisSprite.attr;
81210 axisThickness = axisSprite.thickness;
81211 lineWidth = attr.axisLine ? attr.lineWidth : 0;
81212 halfLineWidth = lineWidth / 2;
81213 labelPadding = Math.max(attr.majorTickSize, attr.minorTickSize) + lineWidth;
81214
81215 axisLabel = me.axesLabels[axisPosition] = axisSurface.add({type: 'composite'});
81216 axisLabel.labelRect = axisLabel.add(Ext.apply({
81217 type: 'rect',
81218 fillStyle: 'white',
81219 x: axisPosition === 'right' ? lineWidth : axisSurface.roundPixel(axisWidth - axisThickness - labelPadding) - halfLineWidth,
81220 y: axisPosition === 'bottom' ? lineWidth : axisSurface.roundPixel(axisHeight - axisThickness - labelPadding) - lineWidth,
81221 width: axisPosition === 'left' ? axisThickness - halfLineWidth + labelPadding : axisThickness + labelPadding,
81222 height: axisPosition === 'top' ? axisThickness + labelPadding : axisThickness + labelPadding
81223 }, axesConfig.rect || axesConfig[axisPosition].rect));
81224 axisLabel.labelText = axisLabel.add(Ext.apply(Ext.Object.chain(axis.config.label), axesConfig.label || axesConfig[axisPosition].label, {
81225 type: 'text',
81226 x: (function () {
81227 switch (axisPosition) {
81228 case 'left':
81229 return axisWidth - labelPadding - halfLineWidth - axisThickness / 2;
81230 case 'right':
81231 return axisThickness / 2 + labelPadding - halfLineWidth;
81232 default:
81233 return 0;
81234 }
81235 })(),
81236 y: (function () {
81237 switch (axisPosition) {
81238 case 'top':
81239 return axisHeight - labelPadding - halfLineWidth - axisThickness / 2;
81240 case 'bottom':
81241 return axisThickness / 2 + labelPadding;
81242 default:
81243 return 0;
81244 }
81245 })()
81246 }));
81247 }
81248 return false;
81249 }
81250
81251 },
81252
81253 onGesture: function (e) {
81254 var me = this;
81255 if (me.getLocks()[me.getGesture()] !== me) {
81256 return;
81257 }
81258 var chart = me.getChart(),
81259 surface = chart.getSurface('overlay-surface'),
81260 region = Ext.Array.slice(chart.getInnerRegion()),
81261 padding = chart.getInnerPadding(),
81262 px = padding.left,
81263 py = padding.top,
81264 chartWidth = region[2],
81265 chartHeight = region[3],
81266 xy = chart.element.getXY(),
81267 x = e.pageX - xy[0] - region[0],
81268 y = e.pageY - xy[1] - region[1],
81269 axes = chart.getAxes(),
81270 axis, axisPosition, axisAlignment, axisSurface, axisSprite, axisMatrix,
81271 axisLayoutContext, axisSegmenter,
81272 axisLabel, labelBBox, textPadding,
81273 xx, yy, dx, dy,
81274 xValue, yValue,
81275 text,
81276 i;
81277
81278 if (x < 0) {
81279 x = 0;
81280 } else if (x > chartWidth) {
81281 x = chartWidth;
81282 }
81283 if (y < 0) {
81284 y = 0;
81285 } else if (y > chartHeight) {
81286 y = chartHeight;
81287 }
81288 x += px;
81289 y += py;
81290
81291 for (i = 0; i < axes.length; i++) {
81292 axis = axes[i];
81293 axisPosition = axis.getPosition();
81294 axisAlignment = axis.getAlignment();
81295 axisSurface = axis.getSurface();
81296 axisSprite = axis.getSprites()[0];
81297 axisMatrix = axisSprite.attr.matrix;
81298 textPadding = axisSprite.attr.textPadding * 2;
81299 axisLabel = me.axesLabels[axisPosition];
81300 axisLayoutContext = axisSprite.getLayoutContext();
81301 axisSegmenter = axis.getSegmenter();
81302
81303 if (axisLabel) {
81304 if (axisAlignment === 'vertical') {
81305 yy = axisMatrix.getYY();
81306 dy = axisMatrix.getDY();
81307 yValue = (y - dy - py) / yy;
81308 if (axis.getLayout() instanceof Ext.chart.axis.layout.Discrete) {
81309 y = Math.round(yValue) * yy + dy + py;
81310 yValue = axisSegmenter.from(Math.round(yValue));
81311 yValue = axisSprite.attr.data[yValue];
81312 } else {
81313 yValue = axisSegmenter.from(yValue);
81314 }
81315 text = axisSegmenter.renderer(yValue, axisLayoutContext);
81316
81317 axisLabel.setAttributes({translationY: y - py});
81318 axisLabel.labelText.setAttributes({text: text});
81319 labelBBox = axisLabel.labelText.getBBox();
81320 axisLabel.labelRect.setAttributes({
81321 height: labelBBox.height + textPadding,
81322 y: -(labelBBox.height + textPadding) / 2
81323 });
81324 axisSurface.renderFrame();
81325 } else {
81326 xx = axisMatrix.getXX();
81327 dx = axisMatrix.getDX();
81328 xValue = (x - dx - px) / xx;
81329 if (axis.getLayout() instanceof Ext.chart.axis.layout.Discrete) {
81330 x = Math.round(xValue) * xx + dx + px;
81331 xValue = axisSegmenter.from(Math.round(xValue));
81332 xValue = axisSprite.attr.data[xValue];
81333 } else {
81334 xValue = axisSegmenter.from(xValue);
81335 }
81336 text = axisSegmenter.renderer(xValue, axisLayoutContext);
81337
81338 axisLabel.setAttributes({translationX: x - px});
81339 axisLabel.labelText.setAttributes({text: text});
81340 labelBBox = axisLabel.labelText.getBBox();
81341 axisLabel.labelRect.setAttributes({
81342 width: labelBBox.width + textPadding,
81343 x: -(labelBBox.width + textPadding) / 2
81344 });
81345 axisSurface.renderFrame();
81346 }
81347 }
81348 }
81349 me.horizontalLine.setAttributes({y: y});
81350 me.verticalLine.setAttributes({x: x});
81351 surface.renderFrame();
81352 return false;
81353 },
81354
81355 onGestureEnd: function (e) {
81356 var me = this,
81357 chart = me.getChart(),
81358 surface = chart.getSurface('overlay-surface'),
81359 axes = chart.getAxes(),
81360 axis, axisPosition, axisSurface, axisLabel,
81361 i;
81362
81363 surface.remove(me.verticalLine);
81364 surface.remove(me.horizontalLine);
81365
81366 for (i = 0; i < axes.length; i++) {
81367 axis = axes[i];
81368 axisPosition = axis.getPosition();
81369 axisSurface = axis.getSurface();
81370 axisLabel = me.axesLabels[axisPosition];
81371 if (axisLabel) {
81372 delete me.axesLabels[axisPosition];
81373 axisSurface.remove(axisLabel);
81374 }
81375 axisSurface.renderFrame();
81376 }
81377
81378 surface.renderFrame();
81379 me.unlockEvents(me.getGesture());
81380 }
81381
81382 });
81383
81384 /**
81385 * @class Ext.chart.interactions.ItemHighlight
81386 * @extends Ext.chart.interactions.Abstract
81387 *
81388 * The ItemHighlight interaction allows the user to highlight series items in the chart.
81389 */
81390 Ext.define('Ext.chart.interactions.ItemHighlight', {
81391
81392 extend: Ext.chart.interactions.Abstract ,
81393
81394 type: 'itemhighlight',
81395 alias: 'interaction.itemhighlight',
81396
81397 config: {
81398 /**
81399 * @cfg {String} gesture
81400 * Defines the gesture type that should trigger item highlighting.
81401 */
81402 gesture: 'tap'
81403 },
81404
81405 getGestures: function () {
81406 var gestures = {};
81407 gestures['item' + this.getGesture()] = 'onGesture';
81408 gestures[this.getGesture()] = 'onFailedGesture';
81409 return gestures;
81410 },
81411
81412 onGesture: function (series, item, e) {
81413 e.highlightItem = item;
81414 return false;
81415 },
81416
81417 onFailedGesture: function (e) {
81418 this.getChart().setHighlightItem(e.highlightItem || null);
81419 this.sync();
81420 }
81421 });
81422
81423 /**
81424 * The ItemInfo interaction allows displaying detailed information about a series data
81425 * point in a popup panel.
81426 *
81427 * To attach this interaction to a chart, include an entry in the chart's
81428 * {@link Ext.chart.AbstractChart#interactions interactions} config with the `iteminfo` type:
81429 *
81430 * new Ext.chart.AbstractChart({
81431 * renderTo: Ext.getBody(),
81432 * width: 800,
81433 * height: 600,
81434 * store: store1,
81435 * axes: [ ...some axes options... ],
81436 * series: [ ...some series options... ],
81437 * interactions: [{
81438 * type: 'iteminfo',
81439 * listeners: {
81440 * show: function(me, item, panel) {
81441 * panel.setHtml('Stock Price: $' + item.record.get('price'));
81442 * }
81443 * }
81444 * }]
81445 * });
81446 */
81447 Ext.define('Ext.chart.interactions.ItemInfo', {
81448
81449 extend: Ext.chart.interactions.Abstract ,
81450
81451 type: 'iteminfo',
81452 alias: 'interaction.iteminfo',
81453
81454 /**
81455 * @event show
81456 * Fires when the info panel is shown.
81457 * @param {Ext.chart.interactions.ItemInfo} this The interaction instance
81458 * @param {Object} item The item whose info is being displayed
81459 * @param {Ext.Panel} panel The panel for displaying the info
81460 */
81461
81462 config: {
81463 /**
81464 * @cfg {String} gesture
81465 * Defines the gesture type that should trigger the item info panel to be displayed.
81466 */
81467 gesture: 'itemtap',
81468
81469 /**
81470 * @cfg {Object} panel
81471 * An optional set of configuration overrides for the {@link Ext.Panel} that gets
81472 * displayed. This object will be merged with the default panel configuration.
81473 */
81474 panel: {
81475 modal: true,
81476 centered: true,
81477 width: 250,
81478 height: 300,
81479 styleHtmlContent: true,
81480 scrollable: 'vertical',
81481 hideOnMaskTap: true,
81482 fullscreen: false,
81483 hidden: true,
81484 zIndex: 30,
81485 items: [
81486 {
81487 docked: 'top',
81488 xtype: 'toolbar',
81489 title: 'Item Detail'
81490 }
81491 ]
81492 }
81493 },
81494
81495 applyPanel: function (panel, oldPanel) {
81496 return Ext.factory(panel, 'Ext.Panel', oldPanel);
81497 },
81498
81499 updatePanel: function (panel, oldPanel) {
81500 if (panel) {
81501 panel.on('hide', "reset", this);
81502 }
81503 if (oldPanel) {
81504 oldPanel.un('hide', "reset", this);
81505 }
81506 },
81507
81508 onGesture: function (series, item) {
81509 var me = this,
81510 panel = me.getPanel();
81511 me.item = item;
81512 me.fireEvent('show', me, item, panel);
81513 Ext.Viewport.add(panel);
81514 panel.show('pop');
81515 series.setAttributesForItem(item, { highlighted: true });
81516 me.sync();
81517 return false;
81518 },
81519
81520 reset: function () {
81521 var me = this,
81522 item = me.item;
81523 if (item) {
81524 item.series.setAttributesForItem(item, { highlighted: false });
81525 delete me.item;
81526 me.sync();
81527 }
81528 }
81529 });
81530
81531 /**
81532 * @private
81533 */
81534 Ext.define('Ext.util.Offset', {
81535
81536 /* Begin Definitions */
81537
81538 statics: {
81539 fromObject: function(obj) {
81540 return new this(obj.x, obj.y);
81541 }
81542 },
81543
81544 /* End Definitions */
81545
81546 constructor: function(x, y) {
81547 this.x = (x != null && !isNaN(x)) ? x : 0;
81548 this.y = (y != null && !isNaN(y)) ? y : 0;
81549
81550 return this;
81551 },
81552
81553 copy: function() {
81554 return new Ext.util.Offset(this.x, this.y);
81555 },
81556
81557 copyFrom: function(p) {
81558 this.x = p.x;
81559 this.y = p.y;
81560 },
81561
81562 toString: function() {
81563 return "Offset[" + this.x + "," + this.y + "]";
81564 },
81565
81566 equals: function(offset) {
81567 //<debug>
81568 if(!(offset instanceof this.statics())) {
81569 Ext.Error.raise('Offset must be an instance of Ext.util.Offset');
81570 }
81571 //</debug>
81572
81573 return (this.x == offset.x && this.y == offset.y);
81574 },
81575
81576 round: function(to) {
81577 if (!isNaN(to)) {
81578 var factor = Math.pow(10, to);
81579 this.x = Math.round(this.x * factor) / factor;
81580 this.y = Math.round(this.y * factor) / factor;
81581 } else {
81582 this.x = Math.round(this.x);
81583 this.y = Math.round(this.y);
81584 }
81585 },
81586
81587 isZero: function() {
81588 return this.x == 0 && this.y == 0;
81589 }
81590 });
81591
81592 /**
81593 * Represents a rectangular region and provides a number of utility methods
81594 * to compare regions.
81595 */
81596 Ext.define('Ext.util.Region', {
81597
81598
81599
81600 statics: {
81601 /**
81602 * @static
81603 * Retrieves an Ext.util.Region for a particular element.
81604 * @param {String/HTMLElement/Ext.Element} el The element or its ID.
81605 * @return {Ext.util.Region} region
81606 */
81607 getRegion: function(el) {
81608 return Ext.fly(el).getPageBox(true);
81609 },
81610
81611 /**
81612 * @static
81613 * Creates new Region from an object:
81614 *
81615 * Ext.util.Region.from({top: 0, right: 5, bottom: 3, left: -1});
81616 * // the above is equivalent to:
81617 * new Ext.util.Region(0, 5, 3, -1);
81618 *
81619 * @param {Object} o An object with `top`, `right`, `bottom`, and `left` properties.
81620 * @param {Number} o.top
81621 * @param {Number} o.right
81622 * @param {Number} o.bottom
81623 * @param {Number} o.left
81624 * @return {Ext.util.Region} The region constructed based on the passed object.
81625 */
81626 from: function(o) {
81627 return new this(o.top, o.right, o.bottom, o.left);
81628 }
81629 },
81630
81631 /**
81632 * Creates new Region.
81633 * @param {Number} top Top
81634 * @param {Number} right Right
81635 * @param {Number} bottom Bottom
81636 * @param {Number} left Left
81637 */
81638 constructor: function(top, right, bottom, left) {
81639 var me = this;
81640 me.top = top;
81641 me[1] = top;
81642 me.right = right;
81643 me.bottom = bottom;
81644 me.left = left;
81645 me[0] = left;
81646 },
81647
81648 /**
81649 * Checks if this region completely contains the region that is passed in.
81650 * @param {Ext.util.Region} region
81651 * @return {Boolean}
81652 */
81653 contains: function(region) {
81654 var me = this;
81655 return (region.left >= me.left &&
81656 region.right <= me.right &&
81657 region.top >= me.top &&
81658 region.bottom <= me.bottom);
81659
81660 },
81661
81662 /**
81663 * Checks if this region intersects the region passed in.
81664 * @param {Ext.util.Region} region
81665 * @return {Ext.util.Region/Boolean} Returns the intersected region or `false` if there is no intersection.
81666 */
81667 intersect: function(region) {
81668 var me = this,
81669 t = Math.max(me.top, region.top),
81670 r = Math.min(me.right, region.right),
81671 b = Math.min(me.bottom, region.bottom),
81672 l = Math.max(me.left, region.left);
81673
81674 if (b > t && r > l) {
81675 return new Ext.util.Region(t, r, b, l);
81676 }
81677 else {
81678 return false;
81679 }
81680 },
81681
81682 /**
81683 * Returns the smallest region that contains the current AND `targetRegion`.
81684 * @param {Ext.util.Region} region
81685 * @return {Ext.util.Region}
81686 */
81687 union: function(region) {
81688 var me = this,
81689 t = Math.min(me.top, region.top),
81690 r = Math.max(me.right, region.right),
81691 b = Math.max(me.bottom, region.bottom),
81692 l = Math.min(me.left, region.left);
81693
81694 return new Ext.util.Region(t, r, b, l);
81695 },
81696
81697 /**
81698 * Modifies the current region to be constrained to the `targetRegion`.
81699 * @param {Ext.util.Region} targetRegion
81700 * @return {Ext.util.Region} this
81701 */
81702 constrainTo: function(targetRegion) {
81703 var me = this,
81704 constrain = Ext.util.Numbers.constrain;
81705 me.top = constrain(me.top, targetRegion.top, targetRegion.bottom);
81706 me.bottom = constrain(me.bottom, targetRegion.top, targetRegion.bottom);
81707 me.left = constrain(me.left, targetRegion.left, targetRegion.right);
81708 me.right = constrain(me.right, targetRegion.left, targetRegion.right);
81709 return me;
81710 },
81711
81712 /**
81713 * Modifies the current region to be adjusted by offsets.
81714 * @param {Number} top Top offset
81715 * @param {Number} right Right offset
81716 * @param {Number} bottom Bottom offset
81717 * @param {Number} left Left offset
81718 * @return {Ext.util.Region} this
81719 * @chainable
81720 */
81721 adjust: function(top, right, bottom, left) {
81722 var me = this;
81723 me.top += top;
81724 me.left += left;
81725 me.right += right;
81726 me.bottom += bottom;
81727 return me;
81728 },
81729
81730 /**
81731 * Get the offset amount of a point outside the region.
81732 * @param {String/Object} axis optional.
81733 * @param {Ext.util.Point} p The point.
81734 * @return {Ext.util.Region}
81735 */
81736 getOutOfBoundOffset: function(axis, p) {
81737 if (!Ext.isObject(axis)) {
81738 if (axis == 'x') {
81739 return this.getOutOfBoundOffsetX(p);
81740 } else {
81741 return this.getOutOfBoundOffsetY(p);
81742 }
81743 } else {
81744 var d = new Ext.util.Offset();
81745 d.x = this.getOutOfBoundOffsetX(axis.x);
81746 d.y = this.getOutOfBoundOffsetY(axis.y);
81747 return d;
81748 }
81749
81750 },
81751
81752 /**
81753 * Get the offset amount on the x-axis.
81754 * @param {Number} p The offset.
81755 * @return {Number}
81756 */
81757 getOutOfBoundOffsetX: function(p) {
81758 if (p <= this.left) {
81759 return this.left - p;
81760 } else if (p >= this.right) {
81761 return this.right - p;
81762 }
81763
81764 return 0;
81765 },
81766
81767 /**
81768 * Get the offset amount on the y-axis.
81769 * @param {Number} p The offset.
81770 * @return {Number}
81771 */
81772 getOutOfBoundOffsetY: function(p) {
81773 if (p <= this.top) {
81774 return this.top - p;
81775 } else if (p >= this.bottom) {
81776 return this.bottom - p;
81777 }
81778
81779 return 0;
81780 },
81781
81782 /**
81783 * Check whether the point / offset is out of bounds.
81784 * @param {String} axis optional
81785 * @param {Ext.util.Point/Number} p The point / offset.
81786 * @return {Boolean}
81787 */
81788 isOutOfBound: function(axis, p) {
81789 if (!Ext.isObject(axis)) {
81790 if (axis == 'x') {
81791 return this.isOutOfBoundX(p);
81792 } else {
81793 return this.isOutOfBoundY(p);
81794 }
81795 } else {
81796 p = axis;
81797 return (this.isOutOfBoundX(p.x) || this.isOutOfBoundY(p.y));
81798 }
81799 },
81800
81801 /**
81802 * Check whether the offset is out of bound in the x-axis.
81803 * @param {Number} p The offset.
81804 * @return {Boolean}
81805 */
81806 isOutOfBoundX: function(p) {
81807 return (p < this.left || p > this.right);
81808 },
81809
81810 /**
81811 * Check whether the offset is out of bound in the y-axis.
81812 * @param {Number} p The offset.
81813 * @return {Boolean}
81814 */
81815 isOutOfBoundY: function(p) {
81816 return (p < this.top || p > this.bottom);
81817 },
81818
81819 /**
81820 * Restrict a point within the region by a certain factor.
81821 * @param {String} axis Optional
81822 * @param {Ext.util.Point/Object} p
81823 * @param {Number} factor
81824 * @return {Ext.util.Point/Object/Number}
81825 * @private
81826 */
81827 restrict: function(axis, p, factor) {
81828 if (Ext.isObject(axis)) {
81829 var newP;
81830
81831 factor = p;
81832 p = axis;
81833
81834 if (p.copy) {
81835 newP = p.copy();
81836 }
81837 else {
81838 newP = {
81839 x: p.x,
81840 y: p.y
81841 };
81842 }
81843
81844 newP.x = this.restrictX(p.x, factor);
81845 newP.y = this.restrictY(p.y, factor);
81846 return newP;
81847 } else {
81848 if (axis == 'x') {
81849 return this.restrictX(p, factor);
81850 } else {
81851 return this.restrictY(p, factor);
81852 }
81853 }
81854 },
81855
81856 /*
81857 * Restrict an offset within the region by a certain factor, on the x-axis.
81858 * @param {Number} p
81859 * @param {Number} [factor=1] (optional) The factor.
81860 * @return {Number}
81861 */
81862 restrictX: function(p, factor) {
81863 if (!factor) {
81864 factor = 1;
81865 }
81866
81867 if (p <= this.left) {
81868 p -= (p - this.left) * factor;
81869 }
81870 else if (p >= this.right) {
81871 p -= (p - this.right) * factor;
81872 }
81873 return p;
81874 },
81875
81876 /*
81877 * Restrict an offset within the region by a certain factor, on the y-axis.
81878 * @param {Number} p
81879 * @param {Number} [factor=1] (optional) The factor.
81880 * @return {Number}
81881 */
81882 restrictY: function(p, factor) {
81883 if (!factor) {
81884 factor = 1;
81885 }
81886
81887 if (p <= this.top) {
81888 p -= (p - this.top) * factor;
81889 }
81890 else if (p >= this.bottom) {
81891 p -= (p - this.bottom) * factor;
81892 }
81893 return p;
81894 },
81895
81896 /*
81897 * Get the width / height of this region.
81898 * @return {Object} An object with `width` and `height` properties.
81899 * @return {Number} return.width
81900 * @return {Number} return.height
81901 */
81902 getSize: function() {
81903 return {
81904 width: this.right - this.left,
81905 height: this.bottom - this.top
81906 };
81907 },
81908
81909 /**
81910 * Copy a new instance.
81911 * @return {Ext.util.Region}
81912 */
81913 copy: function() {
81914 return new Ext.util.Region(this.top, this.right, this.bottom, this.left);
81915 },
81916
81917 /**
81918 * Dump this to an eye-friendly string, great for debugging.
81919 * @return {String} For example `Region[0,1,3,2]`.
81920 */
81921 toString: function() {
81922 return "Region[" + this.top + "," + this.right + "," + this.bottom + "," + this.left + "]";
81923 },
81924
81925
81926 /**
81927 * Translate this region by the given offset amount.
81928 * @param {Object} offset
81929 * @return {Ext.util.Region} This Region.
81930 * @chainable
81931 */
81932 translateBy: function(offset) {
81933 this.left += offset.x;
81934 this.right += offset.x;
81935 this.top += offset.y;
81936 this.bottom += offset.y;
81937
81938 return this;
81939 },
81940
81941 /**
81942 * Round all the properties of this region.
81943 * @return {Ext.util.Region} This Region.
81944 * @chainable
81945 */
81946 round: function() {
81947 this.top = Math.round(this.top);
81948 this.right = Math.round(this.right);
81949 this.bottom = Math.round(this.bottom);
81950 this.left = Math.round(this.left);
81951
81952 return this;
81953 },
81954
81955 /**
81956 * Check whether this region is equivalent to the given region.
81957 * @param {Ext.util.Region} region The region to compare with.
81958 * @return {Boolean}
81959 */
81960 equals: function(region) {
81961 return (this.top == region.top && this.right == region.right && this.bottom == region.bottom && this.left == region.left)
81962 }
81963 });
81964
81965 /**
81966 * The PanZoom interaction allows the user to navigate the data for one or more chart
81967 * axes by panning and/or zooming. Navigation can be limited to particular axes. Zooming is
81968 * performed by pinching on the chart or axis area; panning is performed by single-touch dragging.
81969 *
81970 * For devices which do not support multiple-touch events, zooming can not be done via pinch gestures; in this case the
81971 * interaction will allow the user to perform both zooming and panning using the same single-touch drag gesture.
81972 * {@link #modeToggleButton} provides a button to indicate and toggle between two modes.
81973 *
81974 * @example preview
81975 * var lineChart = new Ext.chart.CartesianChart({
81976 * interactions: [{
81977 * type: 'panzoom',
81978 * zoomOnPanGesture: true
81979 * }],
81980 * animate: true,
81981 * store: {
81982 * fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
81983 * data: [
81984 * {'name':'metric one', 'data1':10, 'data2':12, 'data3':14, 'data4':8, 'data5':13},
81985 * {'name':'metric two', 'data1':7, 'data2':8, 'data3':16, 'data4':10, 'data5':3},
81986 * {'name':'metric three', 'data1':5, 'data2':2, 'data3':14, 'data4':12, 'data5':7},
81987 * {'name':'metric four', 'data1':2, 'data2':14, 'data3':6, 'data4':1, 'data5':23},
81988 * {'name':'metric five', 'data1':27, 'data2':38, 'data3':36, 'data4':13, 'data5':33}
81989 * ]
81990 * },
81991 * axes: [{
81992 * type: 'numeric',
81993 * position: 'left',
81994 * fields: ['data1'],
81995 * title: {
81996 * text: 'Sample Values',
81997 * fontSize: 15
81998 * },
81999 * grid: true,
82000 * minimum: 0
82001 * }, {
82002 * type: 'category',
82003 * position: 'bottom',
82004 * fields: ['name'],
82005 * title: {
82006 * text: 'Sample Values',
82007 * fontSize: 15
82008 * }
82009 * }],
82010 * series: [{
82011 * type: 'line',
82012 * highlight: {
82013 * size: 7,
82014 * radius: 7
82015 * },
82016 * style: {
82017 * stroke: 'rgb(143,203,203)'
82018 * },
82019 * xField: 'name',
82020 * yField: 'data1',
82021 * marker: {
82022 * type: 'path',
82023 * path: ['M', -2, 0, 0, 2, 2, 0, 0, -2, 'Z'],
82024 * stroke: 'blue',
82025 * lineWidth: 0
82026 * }
82027 * }, {
82028 * type: 'line',
82029 * highlight: {
82030 * size: 7,
82031 * radius: 7
82032 * },
82033 * fill: true,
82034 * xField: 'name',
82035 * yField: 'data3',
82036 * marker: {
82037 * type: 'circle',
82038 * radius: 4,
82039 * lineWidth: 0
82040 * }
82041 * }]
82042 * });
82043 * Ext.Viewport.setLayout('fit');
82044 * Ext.Viewport.add(lineChart);
82045 *
82046 * The configuration object for the `panzoom` interaction type should specify which axes
82047 * will be made navigable via the `axes` config. See the {@link #axes} config documentation
82048 * for details on the allowed formats. If the `axes` config is not specified, it will default
82049 * to making all axes navigable with the default axis options.
82050 *
82051 */
82052 Ext.define('Ext.chart.interactions.PanZoom', {
82053
82054 extend: Ext.chart.interactions.Abstract ,
82055
82056 type: 'panzoom',
82057 alias: 'interaction.panzoom',
82058
82059
82060
82061
82062
82063 config: {
82064
82065 /**
82066 * @cfg {Object/Array} axes
82067 * Specifies which axes should be made navigable. The config value can take the following formats:
82068 *
82069 * - An Object with keys corresponding to the {@link Ext.chart.axis.Axis#position position} of each
82070 * axis that should be made navigable. Each key's value can either be an Object with further
82071 * configuration options for each axis or simply `true` for a default set of options.
82072 *
82073 * {
82074 * type: 'panzoom',
82075 * axes: {
82076 * left: {
82077 * maxZoom: 5,
82078 * allowPan: false
82079 * },
82080 * bottom: true
82081 * }
82082 * }
82083 *
82084 * If using the full Object form, the following options can be specified for each axis:
82085 *
82086 * - minZoom (Number) A minimum zoom level for the axis. Defaults to `1` which is its natural size.
82087 * - maxZoom (Number) A maximum zoom level for the axis. Defaults to `10`.
82088 * - startZoom (Number) A starting zoom level for the axis. Defaults to `1`.
82089 * - allowZoom (Boolean) Whether zooming is allowed for the axis. Defaults to `true`.
82090 * - allowPan (Boolean) Whether panning is allowed for the axis. Defaults to `true`.
82091 * - startPan (Boolean) A starting panning offset for the axis. Defaults to `0`.
82092 *
82093 * - An Array of strings, each one corresponding to the {@link Ext.chart.axis.Axis#position position}
82094 * of an axis that should be made navigable. The default options will be used for each named axis.
82095 *
82096 * {
82097 * type: 'panzoom',
82098 * axes: ['left', 'bottom']
82099 * }
82100 *
82101 * If the `axes` config is not specified, it will default to making all axes navigable with the
82102 * default axis options.
82103 */
82104 axes: {
82105 top: {},
82106 right: {},
82107 bottom: {},
82108 left: {}
82109 },
82110
82111 minZoom: null,
82112
82113 maxZoom: null,
82114
82115 /**
82116 * @cfg {Boolean} showOverflowArrows
82117 * If `true`, arrows will be conditionally shown at either end of each axis to indicate that the
82118 * axis is overflowing and can therefore be panned in that direction. Set this to `false` to
82119 * prevent the arrows from being displayed.
82120 */
82121 showOverflowArrows: true,
82122
82123 /**
82124 * @cfg {Object} overflowArrowOptions
82125 * A set of optional overrides for the overflow arrow sprites' options. Only relevant when
82126 * {@link #showOverflowArrows} is `true`.
82127 */
82128
82129 gesture: 'pinch',
82130
82131 panGesture: 'drag',
82132
82133 zoomOnPanGesture: false,
82134
82135 modeToggleButton: {
82136 cls: ['x-panzoom-toggle', 'x-zooming'],
82137 iconCls: 'expand'
82138 },
82139
82140 hideLabelInGesture: false //Ext.os.is.Android
82141 },
82142
82143 stopAnimationBeforeSync: true,
82144
82145 applyAxes: function (axesConfig, oldAxesConfig) {
82146 return Ext.merge(oldAxesConfig || {}, axesConfig);
82147 },
82148
82149 applyZoomOnPanGesture: function (zoomOnPanGesture) {
82150 this.getChart();
82151 if (this.isMultiTouch()) {
82152 return false;
82153 }
82154 return zoomOnPanGesture;
82155 },
82156
82157 updateZoomOnPanGesture: function (zoomOnPanGesture) {
82158 if (!this.isMultiTouch()) {
82159 var button = this.getModeToggleButton(),
82160 zoomModeCls = Ext.baseCSSPrefix + 'zooming';
82161 if (zoomOnPanGesture) {
82162 button.addCls(zoomModeCls);
82163 if (!button.config.hideText) {
82164 button.setText('Zoom');
82165 }
82166 } else {
82167 button.removeCls(zoomModeCls);
82168 if (!button.config.hideText) {
82169 button.setText('Pan');
82170 }
82171 }
82172 }
82173 },
82174
82175 toggleMode: function () {
82176 var me = this;
82177 if (!me.isMultiTouch()) {
82178 me.setZoomOnPanGesture(!me.getZoomOnPanGesture());
82179 }
82180 },
82181
82182 applyModeToggleButton: function (button, oldButton) {
82183 var me = this,
82184 result = Ext.factory(button, "Ext.Button", oldButton);
82185 if (result && !oldButton) {
82186 result.setHandler(function () {
82187 me.toggleMode();
82188 });
82189 }
82190 return result;
82191 },
82192
82193 getGestures: function () {
82194 var me = this,
82195 gestures = {};
82196 gestures[me.getGesture()] = 'onGesture';
82197 gestures[me.getGesture() + 'start'] = 'onGestureStart';
82198 gestures[me.getGesture() + 'end'] = 'onGestureEnd';
82199 gestures[me.getPanGesture()] = 'onPanGesture';
82200 gestures[me.getPanGesture() + 'start'] = 'onPanGestureStart';
82201 gestures[me.getPanGesture() + 'end'] = 'onPanGestureEnd';
82202 gestures.doubletap = 'onDoubleTap';
82203 return gestures;
82204 },
82205
82206 onDoubleTap: function (e) {
82207
82208 },
82209
82210 onPanGestureStart: function (e) {
82211 if (!e || !e.touches || e.touches.length < 2) { //Limit drags to single touch
82212 var me = this,
82213 region = me.getChart().getInnerRegion(),
82214 xy = me.getChart().element.getXY();
82215 me.startX = e.pageX - xy[0] - region[0];
82216 me.startY = e.pageY - xy[1] - region[1];
82217 me.oldVisibleRanges = null;
82218 me.hideLabels();
82219 me.getChart().suspendThicknessChanged();
82220 me.lockEvents(me.getPanGesture());
82221 return false;
82222 }
82223 },
82224
82225 onPanGesture: function (e) {
82226 if (this.getLocks()[this.getPanGesture()] === this) { //Limit drags to single touch
82227 var me = this,
82228 region = me.getChart().getInnerRegion(),
82229 xy = me.getChart().element.getXY();
82230 if (me.getZoomOnPanGesture()) {
82231 me.transformAxesBy(me.getZoomableAxes(e), 0, 0, (e.pageX - xy[0] - region[0]) / me.startX, me.startY / (e.pageY - xy[1] - region[1]));
82232 } else {
82233 me.transformAxesBy(me.getPannableAxes(e), e.pageX - xy[0] - region[0] - me.startX, e.pageY - xy[1] - region[1] - me.startY, 1, 1);
82234 }
82235 me.sync();
82236 return false;
82237 }
82238 },
82239
82240 onPanGestureEnd: function (e) {
82241 var me = this;
82242 if (this.getLocks()[this.getPanGesture()] === this) {
82243 me.getChart().resumeThicknessChanged();
82244 me.showLabels();
82245 me.sync();
82246 me.unlockEvents(me.getGestures());
82247 return false;
82248 }
82249 },
82250
82251 onGestureStart: function (e) {
82252 if (e.touches && e.touches.length === 2) {
82253 var me = this,
82254 xy = me.getChart().element.getXY(),
82255 region = me.getChart().getInnerRegion(),
82256 x = xy[0] + region[0],
82257 y = xy[1] + region[1],
82258 newPoints = [e.touches[0].point.x - x, e.touches[0].point.y - y, e.touches[1].point.x - x, e.touches[1].point.y - y],
82259 xDistance = Math.max(44, Math.abs(newPoints[2] - newPoints[0])),
82260 yDistance = Math.max(44, Math.abs(newPoints[3] - newPoints[1]));
82261 me.getChart().suspendThicknessChanged();
82262 me.lastZoomDistances = [xDistance, yDistance];
82263 me.lastPoints = newPoints;
82264 me.oldVisibleRanges = null;
82265 me.hideLabels();
82266 me.lockEvents(me.getGesture());
82267 return false;
82268 }
82269 },
82270
82271 onGesture: function (e) {
82272 if (this.getLocks()[this.getGesture()] === this) {
82273 var me = this,
82274 region = me.getChart().getInnerRegion(),
82275 xy = me.getChart().element.getXY(),
82276 x = xy[0] + region[0],
82277 y = xy[1] + region[1],
82278 abs = Math.abs,
82279 lastPoints = me.lastPoints,
82280 newPoints = [e.touches[0].point.x - x, e.touches[0].point.y - y, e.touches[1].point.x - x, e.touches[1].point.y - y],
82281 xDistance = Math.max(44, abs(newPoints[2] - newPoints[0])),
82282 yDistance = Math.max(44, abs(newPoints[3] - newPoints[1])),
82283 lastDistances = this.lastZoomDistances || [xDistance, yDistance],
82284 zoomX = xDistance / lastDistances[0],
82285 zoomY = yDistance / lastDistances[1];
82286
82287 me.transformAxesBy(me.getZoomableAxes(e),
82288 region[2] * (zoomX - 1) / 2 + newPoints[2] - lastPoints[2] * zoomX,
82289 region[3] * (zoomY - 1) / 2 + newPoints[3] - lastPoints[3] * zoomY,
82290 zoomX,
82291 zoomY);
82292 me.sync();
82293 return false;
82294 }
82295 },
82296
82297 onGestureEnd: function (e) {
82298 var me = this;
82299 if (me.getLocks()[me.getGesture()] === me) {
82300 me.getChart().resumeThicknessChanged();
82301 me.showLabels();
82302 me.sync();
82303 me.unlockEvents(me.getGestures());
82304 return false;
82305 }
82306 },
82307
82308 hideLabels: function () {
82309 if (this.getHideLabelInGesture()) {
82310 this.eachInteractiveAxes(function (axis) {
82311 axis.hideLabels();
82312 });
82313 }
82314 },
82315
82316 showLabels: function () {
82317 if (this.getHideLabelInGesture()) {
82318 this.eachInteractiveAxes(function (axis) {
82319 axis.showLabels();
82320 });
82321 }
82322 },
82323
82324 isEventOnAxis: function (e, axis) {
82325 // TODO: right now this uses the current event position but really we want to only
82326 // use the gesture's start event. Pinch does not give that to us though.
82327 var region = axis.getSurface().getRegion();
82328 return region[0] <= e.pageX && e.pageX <= region[0] + region[2] && region[1] <= e.pageY && e.pageY <= region[1] + region[3];
82329 },
82330
82331 getPannableAxes: function (e) {
82332 var me = this,
82333 axisConfigs = me.getAxes(),
82334 axes = me.getChart().getAxes(),
82335 i, ln = axes.length,
82336 result = [], isEventOnAxis = false,
82337 config;
82338
82339 if (e) {
82340 for (i = 0; i < ln; i++) {
82341 if (this.isEventOnAxis(e, axes[i])) {
82342 isEventOnAxis = true;
82343 break;
82344 }
82345 }
82346 }
82347
82348 for (i = 0; i < ln; i++) {
82349 config = axisConfigs[axes[i].getPosition()];
82350 if (config && config.allowPan !== false && (!isEventOnAxis || this.isEventOnAxis(e, axes[i]))) {
82351 result.push(axes[i]);
82352 }
82353 }
82354 return result;
82355 },
82356
82357 getZoomableAxes: function (e) {
82358 var me = this,
82359 axisConfigs = me.getAxes(),
82360 axes = me.getChart().getAxes(),
82361 result = [],
82362 i, ln = axes.length, axis,
82363 isEventOnAxis = false, config;
82364
82365 if (e) {
82366 for (i = 0; i < ln; i++) {
82367 if (this.isEventOnAxis(e, axes[i])) {
82368 isEventOnAxis = true;
82369 break;
82370 }
82371 }
82372 }
82373
82374 for (i = 0; i < ln; i++) {
82375 axis = axes[i];
82376 config = axisConfigs[axis.getPosition()];
82377 if (config && config.allowZoom !== false && (!isEventOnAxis || this.isEventOnAxis(e, axis))) {
82378 result.push(axis);
82379 }
82380 }
82381 return result;
82382 },
82383
82384 eachInteractiveAxes: function (fn) {
82385 var me = this,
82386 axisConfigs = me.getAxes(),
82387 axes = me.getChart().getAxes();
82388 for (var i = 0; i < axes.length; i++) {
82389 if (axisConfigs[axes[i].getPosition()]) {
82390 if (false === fn.call(this, axes[i])) {
82391 return;
82392 }
82393 }
82394 }
82395 },
82396
82397 transformAxesBy: function (axes, panX, panY, sx, sy) {
82398 var region = this.getChart().getInnerRegion(),
82399 axesCfg = this.getAxes(), axisCfg,
82400 oldVisibleRanges = this.oldVisibleRanges,
82401 result = false;
82402
82403 if (!oldVisibleRanges) {
82404 this.oldVisibleRanges = oldVisibleRanges = {};
82405 this.eachInteractiveAxes(function (axis) {
82406 oldVisibleRanges[axis.getId()] = axis.getVisibleRange();
82407 });
82408 }
82409
82410 if (!region) {
82411 return;
82412 }
82413
82414 for (var i = 0; i < axes.length; i++) {
82415 axisCfg = axesCfg[axes[i].getPosition()];
82416 result = this.transformAxisBy(axes[i], oldVisibleRanges[axes[i].getId()], panX, panY, sx, sy, this.minZoom || axisCfg.minZoom, this.maxZoom || axisCfg.maxZoom) || result;
82417 }
82418 return result;
82419 },
82420
82421 transformAxisBy: function (axis, oldVisibleRange, panX, panY, sx, sy, minZoom, maxZoom) {
82422 var me = this,
82423 visibleLength = oldVisibleRange[1] - oldVisibleRange[0],
82424 visibleRange = axis.getVisibleRange(),
82425 actualMinZoom = minZoom || me.getMinZoom() || axis.config.minZoom,
82426 actualMaxZoom = maxZoom || me.getMaxZoom() || axis.config.maxZoom,
82427 region = me.getChart().getInnerRegion(),
82428 left, right;
82429 if (!region) {
82430 return;
82431 }
82432
82433 var isSide = axis.isSide(),
82434 length = isSide ? region[3] : region[2],
82435 pan = isSide ? -panY : panX;
82436 visibleLength /= isSide ? sy : sx;
82437 if (visibleLength < 0) {
82438 visibleLength = -visibleLength;
82439 }
82440
82441 if (visibleLength * actualMinZoom > 1) {
82442 visibleLength = 1;
82443 }
82444
82445 if (visibleLength * actualMaxZoom < 1) {
82446 visibleLength = 1 / actualMaxZoom;
82447 }
82448 left = oldVisibleRange[0];
82449 right = oldVisibleRange[1];
82450
82451 visibleRange = visibleRange[1] - visibleRange[0];
82452 if (visibleLength === visibleRange && visibleRange === 1) {
82453 return;
82454 }
82455 axis.setVisibleRange([
82456 (oldVisibleRange[0] + oldVisibleRange[1] - visibleLength) * 0.5 - pan / length * visibleLength,
82457 (oldVisibleRange[0] + oldVisibleRange[1] + visibleLength) * 0.5 - pan / length * visibleLength
82458 ]);
82459 return (Math.abs(left - axis.getVisibleRange()[0]) > 1e-10 || Math.abs(right - axis.getVisibleRange()[1]) > 1e-10);
82460 },
82461
82462 destroy: function () {
82463 this.setModeToggleButton(null);
82464 this.callSuper();
82465 }
82466 });
82467
82468 /**
82469 * @class Ext.chart.interactions.Rotate
82470 * @extends Ext.chart.interactions.Abstract
82471 *
82472 * The Rotate interaction allows the user to rotate a polar chart about its central point.
82473 *
82474 * @example preview
82475 * var chart = new Ext.chart.PolarChart({
82476 * animate: true,
82477 * interactions: ['rotate'],
82478 * colors: ["#115fa6", "#94ae0a", "#a61120", "#ff8809", "#ffd13e"],
82479 * store: {
82480 * fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
82481 * data: [
82482 * {'name':'metric one', 'data1':10, 'data2':12, 'data3':14, 'data4':8, 'data5':13},
82483 * {'name':'metric two', 'data1':7, 'data2':8, 'data3':16, 'data4':10, 'data5':3},
82484 * {'name':'metric three', 'data1':5, 'data2':2, 'data3':14, 'data4':12, 'data5':7},
82485 * {'name':'metric four', 'data1':2, 'data2':14, 'data3':6, 'data4':1, 'data5':23},
82486 * {'name':'metric five', 'data1':27, 'data2':38, 'data3':36, 'data4':13, 'data5':33}
82487 * ]
82488 * },
82489 * series: [{
82490 * type: 'pie',
82491 * label: {
82492 * field: 'name',
82493 * display: 'rotate'
82494 * },
82495 * xField: 'data3',
82496 * donut: 30
82497 * }]
82498 * });
82499 * Ext.Viewport.setLayout('fit');
82500 * Ext.Viewport.add(chart);
82501 */
82502 Ext.define('Ext.chart.interactions.Rotate', {
82503 extend: Ext.chart.interactions.Abstract ,
82504
82505 type: 'rotate',
82506
82507 alias: 'interaction.rotate',
82508
82509 /**
82510 * @event rotate
82511 * Fires on every tick of the rotation
82512 * @param {Ext.chart.interactions.Rotate} this This interaction.
82513 * @param {Number} angle The new current rotation angle.
82514 */
82515
82516 /**
82517 * @event rotationEnd
82518 * Fires after a user finishes the rotation
82519 * @param {Ext.chart.interactions.Rotate} this This interaction.
82520 * @param {Number} angle The new current rotation angle.
82521 */
82522
82523 config: {
82524 /**
82525 * @cfg {String} gesture
82526 * Defines the gesture type that will be used to rotate the chart. Currently only
82527 * supports `pinch` for two-finger rotation and `drag` for single-finger rotation.
82528 */
82529 gesture: 'rotate',
82530
82531 /**
82532 * @cfg {Number} currentRotation
82533 * Saves the current rotation of the series. Accepts negative values and values > 360 ( / 180 * Math.PI)
82534 * @private
82535 */
82536 currentRotation: 0
82537 },
82538
82539 oldRotations: null,
82540
82541 getGestures: function() {
82542 return {
82543 rotate: 'onRotate',
82544 rotateend: 'onRotate',
82545 dragstart: 'onGestureStart',
82546 drag: 'onGesture',
82547 dragend: 'onGestureEnd'
82548 };
82549 },
82550
82551 getAngle: function(e) {
82552 var me = this,
82553 chart = me.getChart(),
82554 xy = chart.getEventXY(e),
82555 center = chart.getCenter();
82556
82557 return Math.atan2(
82558 xy[1] - center[1],
82559 xy[0] - center[0]
82560 );
82561 },
82562
82563 getEventRadius: function(e) {
82564 var me = this,
82565 chart = me.getChart(),
82566 xy = chart.getEventXY(e),
82567 center = chart.getCenter(),
82568 dx = xy[0] - center[0],
82569 dy = xy[1] - center[1];
82570
82571 return Math.sqrt(dx * dx + dy * dy);
82572 },
82573
82574 onGestureStart: function(e) {
82575 var me = this,
82576 chart = me.getChart(),
82577 radius = chart.getRadius(),
82578 eventRadius = me.getEventRadius(e);
82579
82580 if (radius >= eventRadius) {
82581 me.lockEvents('drag');
82582 me.angle = me.getAngle(e);
82583 me.oldRotations = {};
82584 return false;
82585 }
82586 },
82587
82588 onGesture: function(e) {
82589 var me = this,
82590 chart = me.getChart(),
82591 angle = me.getAngle(e) - me.angle,
82592 axes = chart.getAxes(),
82593 series = chart.getSeries(), seriesItem,
82594 oldRotations = me.oldRotations,
82595 axis, oldRotation, i, ln;
82596
82597 if (me.getLocks().drag === me) {
82598 chart.suspendAnimation();
82599
82600 for (i = 0, ln = axes.length; i < ln; i++) {
82601 axis = axes[i];
82602 oldRotation = oldRotations[axis.getId()] || (oldRotations[axis.getId()] = axis.getRotation());
82603 axis.setRotation(angle + oldRotation);
82604 }
82605
82606 for (i = 0, ln = series.length; i < ln; i++) {
82607 seriesItem = series[i];
82608 oldRotation = oldRotations[seriesItem.getId()] || (oldRotations[seriesItem.getId()] = seriesItem.getRotation());
82609
82610 seriesItem.setRotation(angle + oldRotation);
82611 }
82612
82613 me.setCurrentRotation(angle + oldRotation);
82614
82615 me.fireEvent('rotate', me, me.getCurrentRotation());
82616
82617 me.sync();
82618 chart.resumeAnimation();
82619 return false;
82620 }
82621 },
82622
82623 rotateTo: function(angle) {
82624 var me = this,
82625 chart = me.getChart(),
82626 axes = chart.getAxes(),
82627 series = chart.getSeries(),
82628 i, ln;
82629
82630 chart.suspendAnimation();
82631
82632 for (i = 0, ln = axes.length; i < ln; i++) {
82633 axes[i].setRotation(angle);
82634 }
82635
82636 for (i = 0, ln = series.length; i < ln; i++) {
82637 series[i].setRotation(angle);
82638 }
82639
82640 me.setCurrentRotation(angle);
82641
82642 me.fireEvent('rotate', me, me.getCurrentRotation());
82643
82644 me.sync();
82645 chart.resumeAnimation();
82646 },
82647
82648 onGestureEnd: function(e) {
82649 var me = this;
82650
82651 if (me.getLocks().drag === me) {
82652 me.onGesture(e);
82653 me.unlockEvents('drag');
82654
82655 me.fireEvent('rotationEnd', me, me.getCurrentRotation());
82656
82657 return false;
82658 }
82659 },
82660
82661 onRotate: function(e) {
82662
82663 }
82664 });
82665
82666 /**
82667 * @class Ext.chart.interactions.RotatePie3D
82668 * @extends Ext.chart.interactions.Rotate
82669 *
82670 * A special version of the Rotate interaction used by Pie3D Chart.
82671 */
82672 Ext.define('Ext.chart.interactions.RotatePie3D', {
82673
82674 extend: Ext.chart.interactions.Rotate ,
82675
82676 type: 'rotatePie3d',
82677
82678 alias: 'interaction.rotatePie3d',
82679
82680 getAngle: function (e) {
82681 var me = this,
82682 chart = me.getChart(),
82683 xy = chart.element.getXY(),
82684 region = chart.getMainRegion();
82685 return Math.atan2(e.pageY - xy[1] - region[3] * 0.5, e.pageX - xy[0] - region[2] * 0.5);
82686 }
82687 });
82688
82689 /**
82690 * @abstract
82691 * @class Ext.chart.series.Cartesian
82692 * @extends Ext.chart.series.Series
82693 *
82694 * Common base class for series implementations which plot values using x/y coordinates.
82695 *
82696 * @constructor
82697 */
82698 Ext.define('Ext.chart.series.Cartesian', {
82699 extend: Ext.chart.series.Series ,
82700 config: {
82701 /**
82702 * The field used to access the x axis value from the items from the data source.
82703 *
82704 * @cfg {String} xField
82705 */
82706 xField: null,
82707
82708 /**
82709 * The field(s) used to access the y-axis value(s) of the items from the data source.
82710 *
82711 * @cfg {String|String[]} yField
82712 */
82713 yField: null,
82714
82715 /**
82716 * @cfg {Ext.chart.axis.Axis} xAxis The chart axis bound to the series on the x-axis.
82717 */
82718 xAxis: null,
82719
82720 /**
82721 * @cfg {Ext.chart.axis.Axis} yAxis The chart axis bound to the series on the y-axis.
82722 */
82723 yAxis: null
82724 },
82725
82726 directions: ['X', 'Y'],
82727 fieldCategoryX: ['X'],
82728 fieldCategoryY: ['Y'],
82729
82730 updateXAxis: function (axis) {
82731 axis.processData(this);
82732 },
82733
82734 updateYAxis: function (axis) {
82735 axis.processData(this);
82736 },
82737
82738 coordinateX: function () {
82739 return this.coordinate('X', 0, 2);
82740 },
82741
82742 coordinateY: function () {
82743 return this.coordinate('Y', 1, 2);
82744 },
82745
82746 getItemForPoint: function (x, y) {
82747 if (this.getSprites()) {
82748 var me = this,
82749 sprite = me.getSprites()[0],
82750 store = me.getStore(),
82751 item;
82752
82753 if(me.getHidden()) {
82754 return null;
82755 }
82756 if (sprite) {
82757 var index = sprite.getIndexNearPoint(x, y);
82758 if (index !== -1) {
82759 item = {
82760 series: this,
82761 category: this.getItemInstancing() ? 'items' : 'markers',
82762 index: index,
82763 record: store.getData().items[index],
82764 field: this.getYField(),
82765 sprite: sprite
82766 };
82767 return item;
82768 }
82769 }
82770 }
82771 },
82772
82773 createSprite: function () {
82774 var sprite = this.callSuper(),
82775 xAxis = this.getXAxis();
82776 sprite.setAttributes({flipXY: this.getChart().getFlipXY()});
82777 if (sprite.setAggregator && xAxis && xAxis.getAggregator) {
82778 if (xAxis.getAggregator) {
82779 sprite.setAggregator({strategy: xAxis.getAggregator()});
82780 } else {
82781 sprite.setAggregator({});
82782 }
82783 }
82784 return sprite;
82785 },
82786
82787 getSprites: function () {
82788 var me = this,
82789 chart = this.getChart(),
82790 animation = chart && chart.getAnimate(),
82791 itemInstancing = me.getItemInstancing(),
82792 sprites = me.sprites, sprite;
82793
82794 if (!chart) {
82795 return [];
82796 }
82797
82798 if (!sprites.length) {
82799 sprite = me.createSprite();
82800 } else {
82801 sprite = sprites[0];
82802 }
82803
82804 if (animation) {
82805 me.getLabel().getTemplate().fx.setConfig(animation);
82806 if (itemInstancing) {
82807 sprite.itemsMarker.getTemplate().fx.setConfig(animation);
82808 }
82809 sprite.fx.setConfig(animation);
82810 }
82811 return sprites;
82812 },
82813
82814 provideLegendInfo: function (target) {
82815 var style = this.getStyle();
82816 target.push({
82817 name: this.getTitle() || this.getYField() || this.getId(),
82818 mark: style.fillStyle || style.strokeStyle || 'black',
82819 disabled: false,
82820 series: this.getId(),
82821 index: 0
82822 });
82823 },
82824
82825 getXRange: function () {
82826 return [this.dataRange[0], this.dataRange[2]];
82827 },
82828
82829 getYRange: function () {
82830 return [this.dataRange[1], this.dataRange[3]];
82831 }
82832 })
82833 ;
82834
82835 /**
82836 * @abstract
82837 * @extends Ext.chart.series.Cartesian
82838 * Abstract class for all the stacked cartesian series including area series
82839 * and bar series.
82840 */
82841 Ext.define('Ext.chart.series.StackedCartesian', {
82842
82843 extend: Ext.chart.series.Cartesian ,
82844
82845 config: {
82846 /**
82847 * @cfg {Boolean}
82848 * 'true' to display the series in its stacked configuration.
82849 */
82850 stacked: true,
82851
82852 /**
82853 * @cfg {Array} hidden
82854 */
82855 hidden: []
82856 },
82857
82858 animatingSprites: 0,
82859
82860 updateStacked: function () {
82861 this.processData();
82862 },
82863
82864 coordinateY: function () {
82865 return this.coordinateStacked('Y', 1, 2);
82866 },
82867
82868 getFields: function (fieldCategory) {
82869 var me = this,
82870 fields = [], fieldsItem,
82871 i, ln;
82872 for (i = 0, ln = fieldCategory.length; i < ln; i++) {
82873 fieldsItem = me['get' + fieldCategory[i] + 'Field']();
82874 if (Ext.isArray(fieldsItem)) {
82875 fields.push.apply(fields, fieldsItem);
82876 } else {
82877 fields.push(fieldsItem);
82878 }
82879 }
82880 return fields;
82881 },
82882
82883 updateLabelOverflowPadding: function (labelOverflowPadding) {
82884 this.getLabel().setAttributes({labelOverflowPadding: labelOverflowPadding});
82885 },
82886
82887 getSprites: function () {
82888 var me = this,
82889 chart = this.getChart(),
82890 animation = chart && chart.getAnimate(),
82891 fields = me.getFields(me.fieldCategoryY),
82892 itemInstancing = me.getItemInstancing(),
82893 sprites = me.sprites, sprite,
82894 hidden = me.getHidden(),
82895 spritesCreated = false,
82896 i, length = fields.length;
82897
82898 if (!chart) {
82899 return [];
82900 }
82901
82902 for (i = 0; i < length; i++) {
82903 sprite = sprites[i];
82904 if (!sprite) {
82905 sprite = me.createSprite();
82906 if (chart.getFlipXY()) {
82907 sprite.setAttributes({zIndex: i});
82908 } else {
82909 sprite.setAttributes({zIndex: -i});
82910 }
82911 sprite.setField(fields[i]);
82912 spritesCreated = true;
82913 hidden.push(false);
82914 if (itemInstancing) {
82915 sprite.itemsMarker.getTemplate().setAttributes(me.getOverriddenStyleByIndex(i));
82916 } else {
82917 sprite.setAttributes(me.getStyleByIndex(i));
82918 }
82919 }
82920 if (animation) {
82921 if (itemInstancing) {
82922 sprite.itemsMarker.getTemplate().fx.setConfig(animation);
82923 }
82924 sprite.fx.setConfig(animation);
82925 }
82926 }
82927
82928 if (spritesCreated) {
82929 me.updateHidden(hidden);
82930 }
82931 return sprites;
82932 },
82933
82934 getItemForPoint: function (x, y) {
82935 if (this.getSprites()) {
82936 var me = this,
82937 i, ln, sprite,
82938 itemInstancing = me.getItemInstancing(),
82939 sprites = me.getSprites(),
82940 store = me.getStore(),
82941 hidden = me.getHidden(),
82942 item;
82943
82944 for (i = 0, ln = sprites.length; i < ln; i++) {
82945 if(!hidden[i]) {
82946 sprite = sprites[i];
82947 var index = sprite.getIndexNearPoint(x, y);
82948 if (index !== -1) {
82949 item = {
82950 series: me,
82951 index: index,
82952 category: itemInstancing ? 'items' : 'markers',
82953 record: store.getData().items[index],
82954 field: this.getYField()[i],
82955 sprite: sprite
82956 };
82957 return item;
82958 }
82959 }
82960 }
82961 return null;
82962 }
82963 },
82964
82965 provideLegendInfo: function (target) {
82966 var sprites = this.getSprites(),
82967 title = this.getTitle(),
82968 field = this.getYField(),
82969 hidden = this.getHidden();
82970 for (var i = 0; i < sprites.length; i++) {
82971 target.push({
82972 name: Ext.isArray(this.getTitle()) ? this.getTitle()[i] : (field && field[i]) || this.getId(),
82973 mark: this.getStyleByIndex(i).fillStyle || this.getStyleByIndex(i).strokeStyle || 'black',
82974 disabled: hidden[i],
82975 series: this.getId(),
82976 index: i
82977 });
82978 }
82979 },
82980
82981 onSpriteAnimationStart: function (sprite) {
82982 this.animatingSprites++;
82983 if (this.animatingSprites === 1) {
82984 this.fireEvent('animationstart');
82985 }
82986 },
82987
82988 onSpriteAnimationEnd: function (sprite) {
82989 this.animatingSprites--;
82990 if (this.animatingSprites === 0) {
82991 this.fireEvent('animationend');
82992 }
82993 }
82994 });
82995
82996 /**
82997 * @class Ext.chart.series.sprite.Cartesian
82998 * @extends Ext.draw.sprite.Sprite
82999 *
83000 * Cartesian sprite.
83001 */
83002 Ext.define('Ext.chart.series.sprite.Cartesian', {
83003 extend: Ext.draw.sprite.Sprite ,
83004 mixins: {
83005 markerHolder: Ext.chart.MarkerHolder
83006 },
83007 homogeneous: true,
83008 ascending: true,
83009 inheritableStatics: {
83010 def: {
83011 processors: {
83012 /**
83013 * @cfg {Number} [dataMinX=0] Data minimum on the x-axis.
83014 */
83015 dataMinX: 'number',
83016
83017 /**
83018 * @cfg {Number} [dataMaxX=1] Data maximum on the x-axis.
83019 */
83020 dataMaxX: 'number',
83021
83022 /**
83023 * @cfg {Number} [dataMinY=0] Data minimum on the y-axis.
83024 */
83025 dataMinY: 'number',
83026
83027 /**
83028 * @cfg {Number} [dataMaxY=2] Data maximum on the y-axis.
83029 */
83030 dataMaxY: 'number',
83031
83032 /**
83033 * @cfg {Array} Data range derived from all the series bound to the x-axis.
83034 */
83035 rangeX: 'data',
83036 /**
83037 * @cfg {Array} Data range derived from all the series bound to the y-axis.
83038 */
83039 rangeY: 'data',
83040
83041 /**
83042 * @cfg {Object} [dataY=null] Data items on the y-axis.
83043 */
83044 dataY: 'data',
83045
83046 /**
83047 * @cfg {Object} [dataX=null] Data items on the x-axis.
83048 */
83049 dataX: 'data',
83050
83051 /**
83052 * @cfg {Object} [labels=null] Labels used in the series.
83053 */
83054 labels: 'default',
83055
83056 /**
83057 * @cfg {Number} [labelOverflowPadding=10] Padding around labels to determine overlap.
83058 */
83059 labelOverflowPadding: 'number',
83060
83061 /**
83062 * @cfg {Number} [selectionTolerance=20]
83063 * The distance from the event position to the sprite's data points to trigger interactions (used for 'iteminfo', etc).
83064 */
83065 selectionTolerance: 'number',
83066
83067 /**
83068 * @cfg {Boolean} If flipXY is 'true', the series is flipped.
83069 */
83070 flipXY: 'bool',
83071
83072 renderer: 'default',
83073
83074 // PanZoom information
83075 visibleMinX: 'number',
83076 visibleMinY: 'number',
83077 visibleMaxX: 'number',
83078 visibleMaxY: 'number',
83079 innerWidth: 'number',
83080 innerHeight: 'number'
83081 },
83082 defaults: {
83083 dataY: null,
83084 dataX: null,
83085 dataMinX: 0,
83086 dataMaxX: 1,
83087 dataMinY: 0,
83088 dataMaxY: 1,
83089 labels: null,
83090 labelOverflowPadding: 10,
83091 selectionTolerance: 20,
83092 flipXY: false,
83093 renderer: null,
83094 transformFillStroke: false,
83095
83096 visibleMinX: 0,
83097 visibleMinY: 0,
83098 visibleMaxX: 1,
83099 visibleMaxY: 1,
83100 innerWidth: 1,
83101 innerHeight: 1
83102 },
83103 dirtyTriggers: {
83104 dataX: 'dataX,bbox',
83105 dataY: 'dataY,bbox',
83106 dataMinX: 'bbox',
83107 dataMaxX: 'bbox',
83108 dataMinY: 'bbox',
83109 dataMaxY: 'bbox',
83110 visibleMinX: 'panzoom',
83111 visibleMinY: 'panzoom',
83112 visibleMaxX: 'panzoom',
83113 visibleMaxY: 'panzoom',
83114 innerWidth: 'panzoom',
83115 innerHeight: 'panzoom'
83116 },
83117 updaters: {
83118 dataX: function (attrs) {
83119 this.processDataX();
83120 if (!attrs.dirtyFlags.dataY) {
83121 attrs.dirtyFlags.dataY = [];
83122 }
83123 attrs.dirtyFlags.dataY.push('dataY');
83124 },
83125
83126 dataY: function () {
83127 this.processDataY();
83128 },
83129
83130 panzoom: function (attrs) {
83131 var dx = attrs.visibleMaxX - attrs.visibleMinX,
83132 dy = attrs.visibleMaxY - attrs.visibleMinY,
83133 innerWidth = attrs.flipXY ? attrs.innerHeight : attrs.innerWidth,
83134 innerHeight = !attrs.flipXY ? attrs.innerHeight : attrs.innerWidth;
83135 attrs.translationX = -attrs.visibleMinX * innerWidth / dx;
83136 attrs.translationY = -attrs.visibleMinY * innerHeight / dy;
83137 attrs.scalingX = innerWidth / dx;
83138 attrs.scalingY = innerHeight / dy;
83139 attrs.scalingCenterX = 0;
83140 attrs.scalingCenterY = 0;
83141 this.applyTransformations(true);
83142 }
83143 }
83144 }
83145 },
83146
83147 config: {
83148 /**
83149 * @private
83150 * @cfg {Object} store The store that is passed to the renderer.
83151 */
83152 store: null,
83153
83154 /**
83155 * @cfg {String} field The store field used by the series.
83156 */
83157 field: null
83158 },
83159
83160 processDataY: Ext.emptyFn,
83161
83162 processDataX: Ext.emptyFn,
83163
83164 updatePlainBBox: function (plain) {
83165 var attr = this.attr;
83166 plain.x = attr.dataMinX;
83167 plain.y = attr.dataMinY;
83168 plain.width = attr.dataMaxX - attr.dataMinX;
83169 plain.height = attr.dataMaxY - attr.dataMinY;
83170 },
83171
83172 /**
83173 * Does a binary search of the data on the x-axis using the given key.
83174 * @param {String} key
83175 * @return {*}
83176 */
83177 binarySearch: function (key) {
83178 var dx = this.attr.dataX,
83179 start = 0,
83180 end = dx.length;
83181 if (key <= dx[0]) {
83182 return start;
83183 }
83184 if (key >= dx[end - 1]) {
83185 return end - 1;
83186 }
83187 while (start + 1 < end) {
83188 var mid = (start + end) >> 1,
83189 val = dx[mid];
83190 if (val === key) {
83191 return mid;
83192 } else if (val < key) {
83193 start = mid;
83194 } else {
83195 end = mid;
83196 }
83197 }
83198 return start;
83199 },
83200
83201 render: function (surface, ctx, region) {
83202 var me = this,
83203 attr = me.attr,
83204 flipXY = attr.flipXY,
83205 inverseMatrix = attr.inverseMatrix.clone();
83206
83207 inverseMatrix.appendMatrix(surface.inverseMatrix);
83208
83209 if (attr.dataX === null) {
83210 return;
83211 }
83212 if (attr.dataY === null) {
83213 return;
83214 }
83215
83216 if (inverseMatrix.getXX() * inverseMatrix.getYX() || inverseMatrix.getXY() * inverseMatrix.getYY()) {
83217 console.log('Cartesian Series sprite does not support rotation/sheering');
83218 return;
83219 }
83220
83221 var clip = inverseMatrix.transformList([
83222 [region[0] - 1, region[3] + 1],
83223 [region[0] + region[2] + 1, -1]
83224 ]);
83225
83226 clip = clip[0].concat(clip[1]);
83227
83228 if (clip[2] < clip[0]) {
83229 console.log('Cartesian Series sprite does not supports flipped X.');
83230 // TODO: support it
83231 return;
83232 }
83233 me.renderClipped(surface, ctx, clip, region);
83234 },
83235
83236 /**
83237 * Render the given visible clip range.
83238 * @param {Ext.draw.Surface} surface
83239 * @param {Ext.draw.engine.Canvas/Ext.draw.engine.SvgContext} ctx
83240 * @param {Array} clip
83241 * @param {Arrary} region
83242 */
83243 renderClipped: Ext.emptyFn,
83244
83245 /**
83246 * Get the nearest item index from point (x, y). -1 as not found.
83247 * @param {Number} x
83248 * @param {Number} y
83249 * @return {Number} The index
83250 */
83251 getIndexNearPoint: function (x, y) {
83252 var sprite = this,
83253 mat = sprite.attr.matrix,
83254 dataX = sprite.attr.dataX,
83255 dataY = sprite.attr.dataY,
83256 selectionTolerance = sprite.attr.selectionTolerance,
83257 minX, minY, index = -1,
83258 imat = mat.clone().prependMatrix(this.surfaceMatrix).inverse(),
83259 center = imat.transformPoint([x, y]),
83260 positionLB = imat.transformPoint([x - selectionTolerance, y - selectionTolerance]),
83261 positionTR = imat.transformPoint([x + selectionTolerance, y + selectionTolerance]),
83262 left = Math.min(positionLB[0], positionTR[0]),
83263 right = Math.max(positionLB[0], positionTR[0]),
83264 top = Math.min(positionLB[1], positionTR[1]),
83265 bottom = Math.max(positionLB[1], positionTR[1]);
83266
83267 for (var i = 0; i < dataX.length; i++) {
83268 if (left < dataX[i] && dataX[i] < right && top < dataY[i] && dataY[i] < bottom) {
83269 if (index === -1 || (Math.abs(dataX[i] - center[0]) < minX) && (Math.abs(dataY[i] - center[1]) < minY)) {
83270 minX = Math.abs(dataX[i] - center[0]);
83271 minY = Math.abs(dataY[i] - center[1]);
83272 index = i;
83273 }
83274 }
83275 }
83276
83277 return index;
83278 }
83279 });
83280
83281 /**
83282 * @class Ext.chart.series.sprite.StackedCartesian
83283 * @extends Ext.chart.series.sprite.Cartesian
83284 *
83285 * Stacked cartesian sprite.
83286 */
83287 Ext.define("Ext.chart.series.sprite.StackedCartesian", {
83288 extend: Ext.chart.series.sprite.Cartesian ,
83289 inheritableStatics: {
83290 def: {
83291 processors: {
83292 /**
83293 * @private
83294 * @cfg {Number} [groupCount=1] The number of groups in the series.
83295 */
83296 groupCount: 'number',
83297
83298 /**
83299 * @private
83300 * @cfg {Number} [groupOffset=0] The group index of the series sprite.
83301 */
83302 groupOffset: 'number',
83303
83304 /**
83305 * @private
83306 * @cfg {Object} [dataStartY=null] The starting point of the data used in the series.
83307 */
83308 dataStartY: 'data'
83309 },
83310 defaults: {
83311 selectionTolerance: 20,
83312 groupCount: 1,
83313 groupOffset: 0,
83314 dataStartY: null
83315 },
83316 dirtyTriggers: {
83317 dataStartY: 'dataY,bbox'
83318 }
83319 }
83320 },
83321
83322 //@inheritdoc
83323 getIndexNearPoint: function (x, y) {
83324 var sprite = this,
83325 mat = sprite.attr.matrix,
83326 dataX = sprite.attr.dataX,
83327 dataY = sprite.attr.dataY,
83328 dataStartY = sprite.attr.dataStartY,
83329 selectionTolerance = sprite.attr.selectionTolerance,
83330 minX = 0.5, minY = Infinity, index = -1,
83331 imat = mat.clone().prependMatrix(this.surfaceMatrix).inverse(),
83332 center = imat.transformPoint([x, y]),
83333 positionLB = imat.transformPoint([x - selectionTolerance, y - selectionTolerance]),
83334 positionTR = imat.transformPoint([x + selectionTolerance, y + selectionTolerance]),
83335 dx, dy,
83336 top = Math.min(positionLB[1], positionTR[1]),
83337 bottom = Math.max(positionLB[1], positionTR[1]);
83338
83339 for (var i = 0; i < dataX.length; i++) {
83340 if (Math.min(dataStartY[i], dataY[i]) <= bottom && top <= Math.max(dataStartY[i], dataY[i])) {
83341 dx = Math.abs(dataX[i] - center[0]);
83342 dy = Math.max(-Math.min(dataY[i] - center[1], center[1] - dataStartY[i]), 0);
83343 if (dx < minX && dy <= minY) {
83344 minX = dx;
83345 minY = dy;
83346 index = i;
83347 }
83348 }
83349 }
83350
83351 return index;
83352 }
83353 });
83354
83355 /**
83356 * @class Ext.chart.series.sprite.Area
83357 * @extends Ext.chart.series.sprite.StackedCartesian
83358 *
83359 * Area series sprite.
83360 */
83361 Ext.define("Ext.chart.series.sprite.Area", {
83362 alias: 'sprite.areaSeries',
83363 extend: Ext.chart.series.sprite.StackedCartesian ,
83364
83365 inheritableStatics: {
83366 def: {
83367 processors: {
83368 /**
83369 * @cfg {Boolean} [step=false] 'true' if the area is represented with steps instead of lines.
83370 */
83371 step: 'bool'
83372 },
83373 defaults: {
83374 step: false
83375 }
83376 }
83377 },
83378
83379 renderClipped: function (surface, ctx, clip, clipRegion) {
83380 var me = this,
83381 attr = me.attr,
83382 dataX = attr.dataX,
83383 dataY = attr.dataY,
83384 dataStartY = attr.dataStartY,
83385 matrix = attr.matrix,
83386 x, y, i, lastX, lastY,
83387 xx = matrix.elements[0],
83388 dx = matrix.elements[4],
83389 yy = matrix.elements[3],
83390 dy = matrix.elements[5],
83391 surfaceMatrix = me.surfaceMatrix,
83392 markerCfg = {},
83393 start = Math.max(0, this.binarySearch(clip[0])),
83394 end = Math.min(dataX.length - 1, this.binarySearch(clip[2]) + 1);
83395 ctx.beginPath();
83396
83397 if (attr.step) {
83398 lastY = dataY[start] * yy + dy;
83399 for (i = start; i <= end; i++) {
83400 x = dataX[i] * xx + dx;
83401 y = dataY[i] * yy + dy;
83402 ctx.lineTo(x, lastY);
83403 ctx.lineTo(x, lastY = y);
83404 }
83405 } else {
83406 for (i = start; i <= end; i++) {
83407 x = dataX[i] * xx + dx;
83408 y = dataY[i] * yy + dy;
83409 ctx.lineTo(x, y);
83410 }
83411 }
83412
83413 if (dataStartY) {
83414 if (attr.step) {
83415 lastX = dataX[end] * xx + dx;
83416 for (i = end; i >= start; i--) {
83417 x = dataX[i] * xx + dx;
83418 y = dataStartY[i] * yy + dy;
83419 ctx.lineTo(lastX, y);
83420 ctx.lineTo(lastX = x, y);
83421 }
83422 } else {
83423 for (i = end; i >= start; i--) {
83424 x = dataX[i] * xx + dx;
83425 y = dataStartY[i] * yy + dy;
83426 ctx.lineTo(x, y);
83427 }
83428 }
83429 } else {
83430 // dataStartY[i] == 0;
83431 ctx.lineTo(dataX[end] * xx + dx, y);
83432 ctx.lineTo(dataX[end] * xx + dx, dy);
83433 ctx.lineTo(dataX[start] * xx + dx, dy);
83434 ctx.lineTo(dataX[start] * xx + dx, dataY[i] * yy + dy);
83435 }
83436 if (attr.transformFillStroke) {
83437 attr.matrix.toContext(ctx);
83438 }
83439 ctx.fill();
83440 if (attr.transformFillStroke) {
83441 attr.inverseMatrix.toContext(ctx);
83442 }
83443 ctx.beginPath();
83444 if (attr.step) {
83445 for (i = start; i <= end; i++) {
83446 x = dataX[i] * xx + dx;
83447 y = dataY[i] * yy + dy;
83448 ctx.lineTo(x, lastY);
83449 ctx.lineTo(x, lastY = y);
83450 markerCfg.translationX = surfaceMatrix.x(x, y);
83451 markerCfg.translationY = surfaceMatrix.y(x, y);
83452 me.putMarker("markers", markerCfg, i, !attr.renderer);
83453 }
83454 } else {
83455 for (i = start; i <= end; i++) {
83456 x = dataX[i] * xx + dx;
83457 y = dataY[i] * yy + dy;
83458 ctx.lineTo(x, y);
83459 markerCfg.translationX = surfaceMatrix.x(x, y);
83460 markerCfg.translationY = surfaceMatrix.y(x, y);
83461 me.putMarker("markers", markerCfg, i, !attr.renderer);
83462 }
83463 }
83464
83465 if (attr.transformFillStroke) {
83466 attr.matrix.toContext(ctx);
83467 }
83468 ctx.stroke();
83469 }
83470 });
83471
83472 /**
83473 * @class Ext.chart.series.Area
83474 * @extends Ext.chart.series.StackedCartesian
83475 *
83476 * Creates an Area Chart.
83477 *
83478 * @example preview
83479 * var chart = new Ext.chart.CartesianChart({
83480 * animate: true,
83481 * store: {
83482 * fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
83483 * data: [
83484 * {'name':'metric one', 'data1':10, 'data2':12, 'data3':14, 'data4':8, 'data5':13},
83485 * {'name':'metric two', 'data1':7, 'data2':8, 'data3':16, 'data4':10, 'data5':3},
83486 * {'name':'metric three', 'data1':5, 'data2':2, 'data3':14, 'data4':12, 'data5':7},
83487 * {'name':'metric four', 'data1':2, 'data2':14, 'data3':6, 'data4':1, 'data5':23},
83488 * {'name':'metric five', 'data1':27, 'data2':38, 'data3':36, 'data4':13, 'data5':33}
83489 * ]
83490 * },
83491 * axes: [{
83492 * type: 'numeric',
83493 * position: 'left',
83494 * fields: ['data1'],
83495 * title: {
83496 * text: 'Sample Values',
83497 * fontSize: 15
83498 * },
83499 * grid: true,
83500 * minimum: 0
83501 * }, {
83502 * type: 'category',
83503 * position: 'bottom',
83504 * fields: ['name'],
83505 * title: {
83506 * text: 'Sample Values',
83507 * fontSize: 15
83508 * }
83509 * }],
83510 * series: [{
83511 * type: 'area',
83512 * subStyle: {
83513 * fill: ['blue', 'green', 'red']
83514 * },
83515 * xField: 'name',
83516 * yField: ['data1', 'data2', 'data3']
83517 *
83518 * }]
83519 * });
83520 * Ext.Viewport.setLayout('fit');
83521 * Ext.Viewport.add(chart);
83522 */
83523 Ext.define('Ext.chart.series.Area', {
83524
83525 extend: Ext.chart.series.StackedCartesian ,
83526
83527 alias: 'series.area',
83528 type: 'area',
83529 seriesType: 'areaSeries'
83530
83531
83532 });
83533
83534 /**
83535 * @class Ext.chart.series.sprite.Bar
83536 * @extends Ext.chart.series.sprite.StackedCartesian
83537 *
83538 * Draws a sprite used in the bar series.
83539 */
83540 Ext.define('Ext.chart.series.sprite.Bar', {
83541 alias: 'sprite.barSeries',
83542 extend: Ext.chart.series.sprite.StackedCartesian ,
83543
83544 inheritableStatics: {
83545 def: {
83546 processors: {
83547 /**
83548 * @cfg {Number} [minBarWidth=2] The minimum bar width.
83549 */
83550 minBarWidth: 'number',
83551
83552 /**
83553 * @cfg {Number} [maxBarWidth=100] The maximum bar width.
83554 */
83555 maxBarWidth: 'number',
83556
83557 /**
83558 * @cfg {Number} [minGapWidth=5] The minimum gap between bars.
83559 */
83560 minGapWidth: 'number',
83561
83562 /**
83563 * @cfg {Number} [radius=0] The degree of rounding for rounded bars.
83564 */
83565 radius: 'number',
83566
83567 /**
83568 * @cfg {Number} [inGroupGapWidth=3] The gap between grouped bars.
83569 */
83570 inGroupGapWidth: 'number'
83571 },
83572 defaults: {
83573 minBarWidth: 2,
83574 maxBarWidth: 100,
83575 minGapWidth: 5,
83576 inGroupGapWidth: 3,
83577 radius: 0
83578 }
83579 }
83580 },
83581
83582 // TODO: design this more carefully
83583 drawLabel: function (text, dataX, dataStartY, dataY, labelId) {
83584 var me = this,
83585 attr = me.attr,
83586 label = me.getBoundMarker('labels')[0],
83587 labelTpl = label.getTemplate(),
83588 labelCfg = me.labelCfg || (me.labelCfg = {}),
83589 surfaceMatrix = me.surfaceMatrix,
83590 labelOverflowPadding = attr.labelOverflowPadding,
83591 labelDisplay = labelTpl.attr.display,
83592 labelOrientation = labelTpl.attr.orientation,
83593 labelY, halfWidth, labelBox,
83594 changes;
83595
83596 labelBox = me.getMarkerBBox('labels', labelId, true);
83597 labelCfg.text = text;
83598 if (!labelBox) {
83599 me.putMarker('labels', labelCfg, labelId);
83600 labelBox = me.getMarkerBBox('labels', labelId, true);
83601 }
83602 if (!attr.flipXY) {
83603 labelCfg.rotationRads = -Math.PI * 0.5;
83604 } else {
83605 labelCfg.rotationRads = 0;
83606 }
83607 labelCfg.calloutVertical = !attr.flipXY;
83608
83609 switch (labelOrientation) {
83610 case 'horizontal': labelCfg.rotationRads = 0; break;
83611 case 'vertical': labelCfg.rotationRads = -Math.PI * 0.5; break;
83612 }
83613
83614 halfWidth = (labelBox.width / 2 + labelOverflowPadding);
83615 if (dataStartY > dataY) {
83616 halfWidth = -halfWidth;
83617 }
83618
83619 if ((labelOrientation === 'horizontal' && attr.flipXY) || (labelOrientation === 'vertical' && !attr.flipXY) || !labelOrientation) {
83620 labelY = (labelDisplay === 'insideStart') ? dataStartY + halfWidth : dataY - halfWidth;
83621 } else {
83622 labelY = (labelDisplay === 'insideStart') ? dataStartY + labelOverflowPadding * 2 : dataY - labelOverflowPadding * 2;
83623 }
83624 labelCfg.x = surfaceMatrix.x(dataX, labelY);
83625 labelCfg.y = surfaceMatrix.y(dataX, labelY);
83626
83627 labelY = (labelDisplay === 'insideStart') ? dataStartY - halfWidth : dataY + halfWidth;
83628 labelCfg.calloutPlaceX = surfaceMatrix.x(dataX, labelY);
83629 labelCfg.calloutPlaceY = surfaceMatrix.y(dataX, labelY);
83630
83631 labelY = (labelDisplay === 'insideStart') ? dataStartY : dataY;
83632 labelCfg.calloutStartX = surfaceMatrix.x(dataX, labelY);
83633 labelCfg.calloutStartY = surfaceMatrix.y(dataX, labelY);
83634 if (dataStartY > dataY) {
83635 halfWidth = -halfWidth;
83636 }
83637 if (Math.abs(dataY - dataStartY) <= halfWidth * 2 || labelDisplay === 'outside') {
83638 labelCfg.callout = 1;
83639 } else {
83640 labelCfg.callout = 0;
83641 }
83642
83643 if (labelTpl.attr.renderer) {
83644 changes = labelTpl.attr.renderer.call(this, text, label, labelCfg, {store: this.getStore()}, labelId);
83645 if (typeof changes === 'string') {
83646 labelCfg.text = changes;
83647 } else {
83648 Ext.apply(labelCfg, changes);
83649 }
83650 }
83651
83652 me.putMarker('labels', labelCfg, labelId);
83653 },
83654
83655 drawBar: function (ctx, surface, clip, left, top, right, bottom, index) {
83656 var itemCfg = this.itemCfg || (this.itemCfg = {}),
83657 changes;
83658
83659 itemCfg.x = left;
83660 itemCfg.y = top;
83661 itemCfg.width = right - left;
83662 itemCfg.height = bottom - top;
83663 itemCfg.radius = this.attr.radius;
83664
83665 if (this.attr.renderer) {
83666 changes = this.attr.renderer.call(this, this, itemCfg, {store:this.getStore()}, index);
83667 Ext.apply(itemCfg, changes);
83668 }
83669 this.putMarker('items', itemCfg, index, !this.attr.renderer);
83670 },
83671
83672 //@inheritdoc
83673 renderClipped: function (surface, ctx, clip) {
83674 if (this.cleanRedraw) {
83675 return;
83676 }
83677 var me = this,
83678 attr = me.attr,
83679 dataX = attr.dataX,
83680 dataY = attr.dataY,
83681 dataText = attr.labels,
83682 dataStartY = attr.dataStartY,
83683 groupCount = attr.groupCount,
83684 groupOffset = attr.groupOffset - (groupCount - 1) * 0.5,
83685 inGroupGapWidth = attr.inGroupGapWidth,
83686 yLow, yHi,
83687 lineWidth = ctx.lineWidth,
83688 matrix = attr.matrix,
83689 xx = matrix.elements[0],
83690 yy = matrix.elements[3],
83691 dx = matrix.elements[4],
83692 dy = surface.roundPixel(matrix.elements[5]) - 1,
83693 maxBarWidth = xx - attr.minGapWidth,
83694 barWidth = surface.roundPixel(Math.max(attr.minBarWidth, (Math.min(maxBarWidth, attr.maxBarWidth) - inGroupGapWidth * (groupCount - 1)) / groupCount)),
83695 surfaceMatrix = this.surfaceMatrix,
83696 left, right, bottom, top, i, center,
83697 halfLineWidth = 0.5 * attr.lineWidth,
83698 start = Math.max(0, Math.floor(clip[0])),
83699 end = Math.min(dataX.length - 1, Math.ceil(clip[2])),
83700 drawMarkers = dataText && !!this.getBoundMarker('labels');
83701
83702 for (i = start; i <= end; i++) {
83703 yLow = dataStartY ? dataStartY[i] : 0;
83704 yHi = dataY[i];
83705
83706 center = dataX[i] * xx + dx + groupOffset * (barWidth + inGroupGapWidth);
83707 left = surface.roundPixel(center - barWidth / 2) + halfLineWidth;
83708 top = surface.roundPixel(yHi * yy + lineWidth + dy);
83709 right = surface.roundPixel(center + barWidth / 2) - halfLineWidth;
83710 bottom = surface.roundPixel(yLow * yy + lineWidth + dy);
83711
83712 me.drawBar(ctx, surface, clip, left, top - halfLineWidth, right, bottom - halfLineWidth, i);
83713
83714 if (drawMarkers && dataText[i]) {
83715 me.drawLabel(dataText[i], center, bottom, top, i);
83716 }
83717 me.putMarker('markers', {
83718 translationX: surfaceMatrix.x(center, top),
83719 translationY: surfaceMatrix.y(center, top)
83720 }, i, true);
83721 }
83722 },
83723
83724 //@inheritdoc
83725 getIndexNearPoint: function (x, y) {
83726 var sprite = this,
83727 attr = sprite.attr,
83728 dataX = attr.dataX,
83729 surface = sprite.getParent(),
83730 surfaceRegion = surface.getRegion(),
83731 surfaceHeight = surfaceRegion[3],
83732 hitX, hitY, index = -1;
83733
83734 // The "items" sprites that draw the bars work in a reverse vertical coordinate system
83735 // starting with 0 at the bottom and increasing the Y coordinate toward the top.
83736 // See also Ext.chart.series.Bar.getItemForPoint(x,y) regarding the chart's InnerPadding.
83737 //
83738 // TODO: Cleanup the bar sprites.
83739 if (attr.flipXY) {
83740 hitX = surfaceHeight - y;
83741 hitY = x;
83742 } else {
83743 hitX = x;
83744 hitY = surfaceHeight - y;
83745 }
83746
83747 for (var i = 0; i < dataX.length; i++) {
83748 var bbox = sprite.getMarkerBBox('items', i);
83749 if (bbox && hitX >= bbox.x && hitX <= (bbox.x + bbox.width) && hitY >= bbox.y && hitY <= (bbox.y + bbox.height)) {
83750 index = i;
83751 }
83752 }
83753 return index;
83754 }
83755
83756 });
83757
83758 /**
83759 * @class Ext.chart.series.Bar
83760 * @extends Ext.chart.series.StackedCartesian
83761 *
83762 * Creates a Bar Chart.
83763 *
83764 * @example preview
83765 * var chart = new Ext.chart.Chart({
83766 * store: {
83767 * fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
83768 * data: [
83769 * {'name':'metric one', 'data1':10, 'data2':12, 'data3':14, 'data4':8, 'data5':13},
83770 * {'name':'metric two', 'data1':7, 'data2':8, 'data3':16, 'data4':10, 'data5':3},
83771 * {'name':'metric three', 'data1':5, 'data2':2, 'data3':14, 'data4':12, 'data5':7},
83772 * {'name':'metric four', 'data1':2, 'data2':14, 'data3':6, 'data4':1, 'data5':23},
83773 * {'name':'metric five', 'data1':27, 'data2':38, 'data3':36, 'data4':13, 'data5':33}
83774 * ]
83775 * },
83776 * axes: [{
83777 * type: 'numeric',
83778 * position: 'left',
83779 * title: {
83780 * text: 'Sample Values',
83781 * fontSize: 15
83782 * },
83783 * fields: 'data1'
83784 * }, {
83785 * type: 'category',
83786 * position: 'bottom',
83787 * title: {
83788 * text: 'Sample Values',
83789 * fontSize: 15
83790 * },
83791 * fields: 'name'
83792 * }],
83793 * series: [{
83794 * type: 'bar',
83795 * xField: 'name',
83796 * yField: 'data1',
83797 * style: {
83798 * fill: 'blue'
83799 * }
83800 * }]
83801 * });
83802 * Ext.Viewport.setLayout('fit');
83803 * Ext.Viewport.add(chart);
83804 */
83805 Ext.define('Ext.chart.series.Bar', {
83806
83807 extend: Ext.chart.series.StackedCartesian ,
83808
83809 alias: 'series.bar',
83810 type: 'bar',
83811 seriesType: 'barSeries',
83812
83813
83814
83815
83816
83817
83818 config: {
83819 /**
83820 * @private
83821 * @cfg {Object} itemInstancing Sprite template used for series.
83822 */
83823 itemInstancing: {
83824 type: 'rect',
83825 fx: {
83826 customDuration: {
83827 x: 0,
83828 y: 0,
83829 width: 0,
83830 height: 0,
83831 radius: 0
83832 }
83833 }
83834 }
83835 },
83836
83837 getItemForPoint: function (x, y) {
83838 if (this.getSprites()) {
83839 var me = this,
83840 chart = me.getChart(),
83841 padding = chart.getInnerPadding();
83842
83843 // Convert the coordinates because the "items" sprites that draw the bars ignore the chart's InnerPadding.
83844 // See also Ext.chart.series.sprite.Bar.getItemForPoint(x,y) regarding the series's vertical coordinate system.
83845 //
83846 // TODO: Cleanup the bar sprites.
83847 arguments[0] = x - padding.left;
83848 arguments[1] = y + padding.bottom;
83849 return me.callParent(arguments);
83850 }
83851 },
83852
83853 updateXAxis: function (axis) {
83854 axis.setLabelInSpan(true);
83855 this.callSuper(arguments);
83856 },
83857
83858 updateHidden: function (hidden) {
83859 this.callParent(arguments);
83860 this.updateStacked();
83861 },
83862
83863 updateStacked: function (stacked) {
83864 var sprites = this.getSprites(),
83865 ln = sprites.length,
83866 visible = [],
83867 attrs = {}, i;
83868
83869 for (i = 0; i < ln; i++) {
83870 if (!sprites[i].attr.hidden) {
83871 visible.push(sprites[i]);
83872 }
83873 }
83874 ln = visible.length;
83875
83876 if (this.getStacked()) {
83877 attrs.groupCount = 1;
83878 attrs.groupOffset = 0;
83879 for (i = 0; i < ln; i++) {
83880 visible[i].setAttributes(attrs);
83881 }
83882 } else {
83883 attrs.groupCount = visible.length;
83884 for (i = 0; i < ln; i++) {
83885 attrs.groupOffset = i;
83886 visible[i].setAttributes(attrs);
83887 }
83888 }
83889 this.callSuper(arguments);
83890 }
83891 });
83892
83893 /**
83894 * Limited cache is a size limited cache container that stores limited number of objects.
83895 *
83896 * When {@link #get} is called, the container will try to find the object in the list.
83897 * If failed it will call the {@link #feeder} to create that object. If there are too many
83898 * objects in the container, the old ones are removed.
83899 *
83900 * __Note:__ This is not using a Least Recently Used policy due to simplicity and performance consideration.
83901 */
83902 Ext.define("Ext.draw.LimitedCache", {
83903 config: {
83904 /**
83905 * @cfg {Number}
83906 * The amount limit of the cache.
83907 */
83908 limit: 40,
83909
83910 /**
83911 * @cfg {Function}
83912 * Function that generates the object when look-up failed.
83913 * @return {Number}
83914 */
83915 feeder: function () {
83916 return 0;
83917 },
83918
83919 /**
83920 * @cfg {Object}
83921 * The scope for {@link #feeder}
83922 */
83923 scope: null
83924 },
83925 cache: null,
83926
83927 constructor: function (config) {
83928 this.cache = {};
83929 this.cache.list = [];
83930 this.cache.tail = 0;
83931 this.initConfig(config);
83932 },
83933
83934 /**
83935 * Get a cached object.
83936 * @param {String} id
83937 * @param {Mixed...} args Arguments appended to feeder.
83938 * @return {Object}
83939 */
83940 get: function (id) {
83941 // TODO: Implement cache hit optimization
83942 var cache = this.cache,
83943 limit = this.getLimit(),
83944 feeder = this.getFeeder(),
83945 scope = this.getScope() || this;
83946
83947 if (cache[id]) {
83948 return cache[id].value;
83949 }
83950 if (cache.list[cache.tail]) {
83951 delete cache[cache.list[cache.tail].cacheId];
83952 }
83953 cache[id] = cache.list[cache.tail] = {
83954 value: feeder.apply(scope, Array.prototype.slice.call(arguments, 1)),
83955 cacheId: id
83956 };
83957 cache.tail++;
83958 if (cache.tail === limit) {
83959 cache.tail = 0;
83960 }
83961 return cache[id].value;
83962 },
83963
83964 /**
83965 * Clear all the objects.
83966 */
83967 clear: function () {
83968 this.cache = {};
83969 this.cache.list = [];
83970 this.cache.tail = 0;
83971 }
83972 });
83973
83974 /**
83975 * This class we summarize the data and returns it when required.
83976 */
83977 Ext.define("Ext.draw.SegmentTree", {
83978
83979 config: {
83980 strategy: "double"
83981 },
83982
83983 /**
83984 * @private
83985 * @param {Object} result
83986 * @param {Number} last
83987 * @param {Number} dataX
83988 * @param {Number} dataOpen
83989 * @param {Number} dataHigh
83990 * @param {Number} dataLow
83991 * @param {Number} dataClose
83992 */
83993 time: function (result, last, dataX, dataOpen, dataHigh, dataLow, dataClose) {
83994 var start = 0, lastOffset, lastOffsetEnd,
83995 minimum = new Date(dataX[result.startIdx[0]]),
83996 maximum = new Date(dataX[result.endIdx[last - 1]]),
83997 extDate = Ext.Date,
83998 units = [
83999 [extDate.MILLI, 1, 'ms1', null],
84000 [extDate.MILLI, 2, 'ms2', 'ms1'],
84001 [extDate.MILLI, 5, 'ms5', 'ms1'],
84002 [extDate.MILLI, 10, 'ms10', 'ms5'],
84003 [extDate.MILLI, 50, 'ms50', 'ms10'],
84004 [extDate.MILLI, 100, 'ms100', 'ms50'],
84005 [extDate.MILLI, 500, 'ms500', 'ms100'],
84006 [extDate.SECOND, 1, 's1', 'ms500'],
84007 [extDate.SECOND, 10, 's10', 's1'],
84008 [extDate.SECOND, 30, 's30', 's10'],
84009 [extDate.MINUTE, 1, 'mi1', 's10'],
84010 [extDate.MINUTE, 5, 'mi5', 'mi1'],
84011 [extDate.MINUTE, 10, 'mi10', 'mi5'],
84012 [extDate.MINUTE, 30, 'mi30', 'mi10'],
84013 [extDate.HOUR, 1, 'h1', 'mi30'],
84014 [extDate.HOUR, 6, 'h6', 'h1'],
84015 [extDate.HOUR, 12, 'h12', 'h6'],
84016 [extDate.DAY, 1, 'd1', 'h12'],
84017 [extDate.DAY, 7, 'd7', 'd1'],
84018 [extDate.MONTH, 1, 'mo1', 'd1'],
84019 [extDate.MONTH, 3, 'mo3', 'mo1'],
84020 [extDate.MONTH, 6, 'mo6', 'mo3'],
84021 [extDate.YEAR, 1, 'y1', 'mo3'],
84022 [extDate.YEAR, 5, 'y5', 'y1'],
84023 [extDate.YEAR, 10, 'y10', 'y5'],
84024 [extDate.YEAR, 100, 'y100', 'y10']
84025 ], unitIdx, currentUnit,
84026 plainStart = start,
84027 plainEnd = last,
84028 first = false,
84029 startIdxs = result.startIdx,
84030 endIdxs = result.endIdx,
84031 minIdxs = result.minIdx,
84032 maxIdxs = result.maxIdx,
84033 opens = result.open,
84034 closes = result.close,
84035 minXs = result.minX,
84036 minYs = result.minY,
84037 maxXs = result.maxX,
84038 maxYs = result.maxY,
84039 i, current;
84040
84041 for (unitIdx = 0; last > start + 1 && unitIdx < units.length; unitIdx++) {
84042 minimum = new Date(dataX[startIdxs[0]]);
84043 currentUnit = units[unitIdx];
84044 minimum = extDate.align(minimum, currentUnit[0], currentUnit[1]);
84045 if (extDate.diff(minimum, maximum, currentUnit[0]) > dataX.length * 2 * currentUnit[1]) {
84046 continue;
84047 }
84048 if (currentUnit[3] && result.map['time_' + currentUnit[3]]) {
84049 lastOffset = result.map['time_' + currentUnit[3]][0];
84050 lastOffsetEnd = result.map['time_' + currentUnit[3]][1];
84051 } else {
84052 lastOffset = plainStart;
84053 lastOffsetEnd = plainEnd;
84054 }
84055
84056 start = last;
84057 current = minimum;
84058 first = true;
84059
84060 startIdxs[last] = startIdxs[lastOffset];
84061 endIdxs[last] = endIdxs[lastOffset];
84062 minIdxs[last] = minIdxs[lastOffset];
84063 maxIdxs[last] = maxIdxs[lastOffset];
84064 opens[last] = opens[lastOffset];
84065 closes[last] = closes[lastOffset];
84066 minXs[last] = minXs[lastOffset];
84067 minYs[last] = minYs[lastOffset];
84068 maxXs[last] = maxXs[lastOffset];
84069 maxYs[last] = maxYs[lastOffset];
84070 current = Ext.Date.add(current, currentUnit[0], currentUnit[1]);
84071
84072 for (i = lastOffset + 1; i < lastOffsetEnd; i++) {
84073 if (dataX[endIdxs[i]] < +current) {
84074 endIdxs[last] = endIdxs[i];
84075 closes[last] = closes[i];
84076 if (maxYs[i] > maxYs[last]) {
84077 maxYs[last] = maxYs[i];
84078 maxXs[last] = maxXs[i];
84079 maxIdxs[last] = maxIdxs[i];
84080 }
84081 if (minYs[i] < minYs[last]) {
84082 minYs[last] = minYs[i];
84083 minXs[last] = minXs[i];
84084 minIdxs[last] = minIdxs[i];
84085 }
84086 } else {
84087 last++;
84088 startIdxs[last] = startIdxs[i];
84089 endIdxs[last] = endIdxs[i];
84090 minIdxs[last] = minIdxs[i];
84091 maxIdxs[last] = maxIdxs[i];
84092 opens[last] = opens[i];
84093 closes[last] = closes[i];
84094 minXs[last] = minXs[i];
84095 minYs[last] = minYs[i];
84096 maxXs[last] = maxXs[i];
84097 maxYs[last] = maxYs[i];
84098 current = Ext.Date.add(current, currentUnit[0], currentUnit[1]);
84099 }
84100 }
84101 if (last > start) {
84102 result.map['time_' + currentUnit[2]] = [start, last];
84103 }
84104 }
84105 },
84106
84107 /**
84108 * @private
84109 * @param {Object} result
84110 * @param {Number} position
84111 * @param {Number} dataX
84112 * @param {Number} dataOpen
84113 * @param {Number} dataHigh
84114 * @param {Number} dataLow
84115 * @param {Number} dataClose
84116 */
84117 "double": function (result, position, dataX, dataOpen, dataHigh, dataLow, dataClose) {
84118 var offset = 0, lastOffset, step = 1,
84119 i,
84120 startIdx,
84121 endIdx,
84122 minIdx,
84123 maxIdx,
84124 open,
84125 close,
84126 minX,
84127 minY,
84128 maxX,
84129 maxY;
84130 while (position > offset + 1) {
84131 lastOffset = offset;
84132 offset = position;
84133 step += step;
84134 for (i = lastOffset; i < offset; i += 2) {
84135 if (i === offset - 1) {
84136 startIdx = result.startIdx[i];
84137 endIdx = result.endIdx[i];
84138 minIdx = result.minIdx[i];
84139 maxIdx = result.maxIdx[i];
84140 open = result.open[i];
84141 close = result.close[i];
84142 minX = result.minX[i];
84143 minY = result.minY[i];
84144 maxX = result.maxX[i];
84145 maxY = result.maxY[i];
84146 } else {
84147
84148 startIdx = result.startIdx[i];
84149 endIdx = result.endIdx[i + 1];
84150 open = result.open[i];
84151 close = result.close[i];
84152 if (result.minY[i] <= result.minY[i + 1]) {
84153 minIdx = result.minIdx[i];
84154 minX = result.minX[i];
84155 minY = result.minY[i];
84156 } else {
84157 minIdx = result.minIdx[i + 1];
84158 minX = result.minX[i + 1];
84159 minY = result.minY[i + 1];
84160 }
84161 if (result.maxY[i] >= result.maxY[i + 1]) {
84162 maxIdx = result.maxIdx[i];
84163 maxX = result.maxX[i];
84164 maxY = result.maxY[i];
84165 } else {
84166 maxIdx = result.maxIdx[i + 1];
84167 maxX = result.maxX[i + 1];
84168 maxY = result.maxY[i + 1];
84169 }
84170 }
84171 result.startIdx[position] = startIdx;
84172 result.endIdx[position] = endIdx;
84173 result.minIdx[position] = minIdx;
84174 result.maxIdx[position] = maxIdx;
84175 result.open[position] = open;
84176 result.close[position] = close;
84177 result.minX[position] = minX;
84178 result.minY[position] = minY;
84179 result.maxX[position] = maxX;
84180 result.maxY[position] = maxY;
84181 position++;
84182 }
84183 result.map['double_' + step] = [offset, position];
84184 }
84185 },
84186
84187 /**
84188 * @private
84189 */
84190 none: Ext.emptyFn,
84191
84192 /**
84193 * @private
84194 *
84195 * @param {Number} dataX
84196 * @param {Number} dataOpen
84197 * @param {Number} dataHigh
84198 * @param {Number} dataLow
84199 * @param {Number} dataClose
84200 * @return {Object}
84201 */
84202 aggregateData: function (dataX, dataOpen, dataHigh, dataLow, dataClose) {
84203 var length = dataX.length,
84204 startIdx = [],
84205 endIdx = [],
84206 minIdx = [],
84207 maxIdx = [],
84208 open = [],
84209 minX = [],
84210 minY = [],
84211 maxX = [],
84212 maxY = [],
84213 close = [],
84214 result = {
84215 startIdx: startIdx,
84216 endIdx: endIdx,
84217 minIdx: minIdx,
84218 maxIdx: maxIdx,
84219 open: open,
84220 minX: minX,
84221 minY: minY,
84222 maxX: maxX,
84223 maxY: maxY,
84224 close: close
84225 },
84226 i;
84227
84228 for (i = 0; i < length; i++) {
84229 startIdx[i] = i;
84230 endIdx[i] = i;
84231 minIdx[i] = i;
84232 maxIdx[i] = i;
84233 open[i] = dataOpen[i];
84234 minX[i] = dataX[i];
84235 minY[i] = dataLow[i];
84236 maxX[i] = dataX[i];
84237 maxY[i] = dataHigh[i];
84238 close[i] = dataClose[i];
84239 }
84240
84241 result.map = {
84242 original: [0, length]
84243 };
84244 if (length) {
84245 this[this.getStrategy()](result, length, dataX, dataOpen, dataHigh, dataLow, dataClose);
84246 }
84247 return result;
84248 },
84249
84250 /**
84251 * @private
84252 * @param {Object} items
84253 * @param {Number} start
84254 * @param {Number} end
84255 * @param {Number} key
84256 * @return {*}
84257 */
84258 binarySearchMin: function (items, start, end, key) {
84259 var dx = this.dataX;
84260 if (key <= dx[items.startIdx[0]]) {
84261 return start;
84262 }
84263 if (key >= dx[items.startIdx[end - 1]]) {
84264 return end - 1;
84265 }
84266 while (start + 1 < end) {
84267 var mid = (start + end) >> 1,
84268 val = dx[items.startIdx[mid]];
84269 if (val === key) {
84270 return mid;
84271 } else if (val < key) {
84272 start = mid;
84273 } else {
84274 end = mid;
84275 }
84276 }
84277 return start;
84278 },
84279
84280 /**
84281 * @private
84282 * @param {Object} items
84283 * @param {Number} start
84284 * @param {Number} end
84285 * @param {Number} key
84286 * @return {*}
84287 */
84288 binarySearchMax: function (items, start, end, key) {
84289 var dx = this.dataX;
84290 if (key <= dx[items.endIdx[0]]) {
84291 return start;
84292 }
84293 if (key >= dx[items.endIdx[end - 1]]) {
84294 return end - 1;
84295 }
84296 while (start + 1 < end) {
84297 var mid = (start + end) >> 1,
84298 val = dx[items.endIdx[mid]];
84299 if (val === key) {
84300 return mid;
84301 } else if (val < key) {
84302 start = mid;
84303 } else {
84304 end = mid;
84305 }
84306 }
84307 return end;
84308 },
84309
84310 constructor: function (config) {
84311 this.initConfig(config);
84312 },
84313
84314 /**
84315 * Sets the data of the segment tree.
84316 * @param {Number} dataX
84317 * @param {Number} dataOpen
84318 * @param {Number} dataHigh
84319 * @param {Number} dataLow
84320 * @param {Number} dataClose
84321 */
84322 setData: function (dataX, dataOpen, dataHigh, dataLow, dataClose) {
84323 if (!dataHigh) {
84324 dataClose = dataLow = dataHigh = dataOpen;
84325 }
84326 this.dataX = dataX;
84327 this.dataOpen = dataOpen;
84328 this.dataHigh = dataHigh;
84329 this.dataLow = dataLow;
84330 this.dataClose = dataClose;
84331 if (dataX.length === dataHigh.length &&
84332 dataX.length === dataLow.length) {
84333 this.cache = this.aggregateData(dataX, dataOpen, dataHigh, dataLow, dataClose);
84334 }
84335 },
84336
84337 /**
84338 * Returns the minimum range of data that fits the given range and step size.
84339 *
84340 * @param {Number} min
84341 * @param {Number} max
84342 * @param {Number} estStep
84343 * @return {Object} The aggregation information.
84344 * @return {Number} return.start
84345 * @return {Number} return.end
84346 * @return {Object} return.data The aggregated data
84347 */
84348 getAggregation: function (min, max, estStep) {
84349 if (!this.cache) {
84350 return null;
84351 }
84352 var minStep = Infinity,
84353 range = this.dataX[this.dataX.length - 1] - this.dataX[0],
84354 cacheMap = this.cache.map,
84355 result = cacheMap.original,
84356 name, positions, ln, step, minIdx, maxIdx;
84357
84358 for (name in cacheMap) {
84359 positions = cacheMap[name];
84360 ln = positions[1] - positions[0] - 1;
84361 step = range / ln;
84362 if (estStep <= step && step < minStep) {
84363 result = positions;
84364 minStep = step;
84365 }
84366 }
84367 minIdx = Math.max(this.binarySearchMin(this.cache, result[0], result[1], min), result[0]);
84368 maxIdx = Math.min(this.binarySearchMax(this.cache, result[0], result[1], max) + 1, result[1]);
84369 return {
84370 data: this.cache,
84371 start: minIdx,
84372 end: maxIdx
84373 };
84374 }
84375 });
84376
84377 /**
84378 *
84379 */
84380 Ext.define('Ext.chart.series.sprite.Aggregative', {
84381 extend: Ext.chart.series.sprite.Cartesian ,
84382
84383 inheritableStatics: {
84384 def: {
84385 processors: {
84386 /**
84387 * @cfg {Object} [dataHigh=null] Data items representing the high values of the aggregated data.
84388 */
84389 dataHigh: 'data',
84390
84391 /**
84392 * @cfg {Object} [dataLow=null] Data items representing the low values of the aggregated data.
84393 */
84394 dataLow: 'data',
84395
84396 /**
84397 * @cfg {Object} [dataClose=null] Data items representing the closing values of the aggregated data.
84398 */
84399 dataClose: 'data'
84400 },
84401 aliases: {
84402 /**
84403 * @cfg {Object} [dataOpen=null] Data items representing the opening values of the aggregated data.
84404 */
84405 dataOpen: 'dataY'
84406 },
84407 defaults: {
84408 dataHigh: null,
84409 dataLow: null,
84410 dataClose: null
84411 }
84412 }
84413 },
84414
84415 config: {
84416 aggregator: {}
84417 },
84418
84419 applyAggregator: function (aggregator, oldAggr) {
84420 return Ext.factory(aggregator, Ext.draw.SegmentTree, oldAggr);
84421 },
84422
84423 constructor: function () {
84424 this.callSuper(arguments);
84425 },
84426
84427 processDataY: function () {
84428 var me = this,
84429 attr = me.attr,
84430 high = attr.dataHigh,
84431 low = attr.dataLow,
84432 close = attr.dataClose,
84433 open = attr.dataY;
84434 me.callSuper(arguments);
84435 if (attr.dataX && open && open.length > 0) {
84436 if (high) {
84437 me.getAggregator().setData(attr.dataX, attr.dataY, high, low, close);
84438 } else {
84439 me.getAggregator().setData(attr.dataX, attr.dataY);
84440 }
84441 }
84442 },
84443
84444 getGapWidth: function () {
84445 return 1;
84446 },
84447
84448 renderClipped: function (surface, ctx, clip, region) {
84449 var me = this,
84450 aggregates = me.getAggregator() && me.getAggregator().getAggregation(
84451 clip[0],
84452 clip[2],
84453 (clip[2] - clip[0]) / region[2] * me.getGapWidth()
84454 );
84455 if (aggregates) {
84456 me.dataStart = aggregates.data.startIdx[aggregates.start];
84457 me.dataEnd = aggregates.data.endIdx[aggregates.end - 1];
84458
84459 me.renderAggregates(aggregates.data, aggregates.start, aggregates.end, surface, ctx, clip, region);
84460 }
84461 }
84462 });
84463
84464 /**
84465 * @class Ext.chart.series.sprite.CandleStick
84466 * @extends Ext.chart.series.sprite.Aggregative
84467 *
84468 * CandleStick series sprite.
84469 */
84470 Ext.define('Ext.chart.series.sprite.CandleStick', {
84471 alias: 'sprite.candlestickSeries',
84472 extend: Ext.chart.series.sprite.Aggregative ,
84473 inheritableStatics: {
84474 def: {
84475 processors: {
84476 raiseStyle: function (n, o) {
84477 return Ext.merge({}, o || {}, n);
84478 },
84479 dropStyle: function (n, o) {
84480 return Ext.merge({}, o || {}, n);
84481 },
84482
84483 /**
84484 * @cfg {Number} [barWidth=15] The bar width of the candles.
84485 */
84486 barWidth: 'number',
84487
84488 /**
84489 * @cfg {Number} [padding=3] The amount of padding between candles.
84490 */
84491 padding: 'number',
84492
84493 /**
84494 * @cfg {String} [ohlcType='candlestick'] Determines whether candlestick or ohlc is used.
84495 */
84496 ohlcType: 'enums(candlestick,ohlc)'
84497 },
84498 defaults: {
84499 raiseStyle: {
84500 strokeStyle: 'green',
84501 fillStyle: 'green'
84502 },
84503 dropStyle: {
84504 strokeStyle: 'red',
84505 fillStyle: 'red'
84506 },
84507 planar: false,
84508 barWidth: 15,
84509 padding: 3,
84510 lineJoin: 'miter',
84511 miterLimit: 5,
84512 ohlcType: 'candlestick'
84513 },
84514
84515 dirtyTriggers: {
84516 raiseStyle: 'raiseStyle',
84517 dropStyle: 'dropStyle'
84518 },
84519
84520 updaters: {
84521 raiseStyle: function () {
84522 this.raiseTemplate && this.raiseTemplate.setAttributes(this.attr.raiseStyle);
84523 },
84524 dropStyle: function () {
84525 this.dropTemplate && this.dropTemplate.setAttributes(this.attr.dropStyle);
84526 }
84527 }
84528 }
84529 },
84530
84531 candlestick: function (ctx, open, high, low, close, mid, halfWidth) {
84532 var minOC = Math.min(open, close),
84533 maxOC = Math.max(open, close);
84534 ctx.moveTo(mid, low);
84535 ctx.lineTo(mid, maxOC);
84536
84537 ctx.moveTo(mid + halfWidth, maxOC);
84538 ctx.lineTo(mid + halfWidth, minOC);
84539 ctx.lineTo(mid - halfWidth, minOC);
84540 ctx.lineTo(mid - halfWidth, maxOC);
84541 ctx.closePath();
84542
84543 ctx.moveTo(mid, high);
84544 ctx.lineTo(mid, minOC);
84545 },
84546
84547 ohlc: function (ctx, open, high, low, close, mid, halfWidth) {
84548 ctx.moveTo(mid, high);
84549 ctx.lineTo(mid, low);
84550 ctx.moveTo(mid, open);
84551 ctx.lineTo(mid - halfWidth, open);
84552 ctx.moveTo(mid, close);
84553 ctx.lineTo(mid + halfWidth, close);
84554 },
84555
84556 constructor: function () {
84557 this.callSuper(arguments);
84558 this.raiseTemplate = new Ext.draw.sprite.Rect({parent: this});
84559 this.dropTemplate = new Ext.draw.sprite.Rect({parent: this});
84560 },
84561
84562 getGapWidth: function () {
84563 var attr = this.attr,
84564 barWidth = attr.barWidth,
84565 padding = attr.padding;
84566 return barWidth + padding;
84567 },
84568
84569 renderAggregates: function (aggregates, start, end, surface, ctx, clip, region) {
84570 var me = this,
84571 attr = this.attr,
84572 dataX = attr.dataX,
84573 matrix = attr.matrix,
84574 xx = matrix.getXX(),
84575 yy = matrix.getYY(),
84576 dx = matrix.getDX(),
84577 dy = matrix.getDY(),
84578 barWidth = attr.barWidth / xx,
84579 template,
84580 ohlcType = attr.ohlcType,
84581 halfWidth = Math.round(barWidth * 0.5 * xx),
84582 opens = aggregates.open,
84583 highs = aggregates.high,
84584 lows = aggregates.low,
84585 closes = aggregates.close,
84586 maxYs = aggregates.maxY,
84587 minYs = aggregates.minY,
84588 startIdxs = aggregates.startIdx,
84589 open, high, low, close, mid,
84590 i,
84591 pixelAdjust = attr.lineWidth * surface.devicePixelRatio / 2;
84592
84593 pixelAdjust -= Math.floor(pixelAdjust);
84594 ctx.save();
84595 template = this.raiseTemplate;
84596 template.useAttributes(ctx);
84597 ctx.beginPath();
84598 for (i = start; i < end; i++) {
84599 if (opens[i] <= closes[i]) {
84600 open = Math.round(opens[i] * yy + dy) + pixelAdjust;
84601 high = Math.round(maxYs[i] * yy + dy) + pixelAdjust;
84602 low = Math.round(minYs[i] * yy + dy) + pixelAdjust;
84603 close = Math.round(closes[i] * yy + dy) + pixelAdjust;
84604 mid = Math.round(dataX[startIdxs[i]] * xx + dx) + pixelAdjust;
84605 me[ohlcType](ctx, open, high, low, close, mid, halfWidth);
84606 }
84607 }
84608 ctx.fillStroke(template.attr);
84609 ctx.restore();
84610
84611 ctx.save();
84612 template = this.dropTemplate;
84613 template.useAttributes(ctx);
84614 ctx.beginPath();
84615 for (i = start; i < end; i++) {
84616 if (opens[i] > closes[i]) {
84617 open = Math.round(opens[i] * yy + dy) + pixelAdjust;
84618 high = Math.round(maxYs[i] * yy + dy) + pixelAdjust;
84619 low = Math.round(minYs[i] * yy + dy) + pixelAdjust;
84620 close = Math.round(closes[i] * yy + dy) + pixelAdjust;
84621 mid = Math.round(dataX[startIdxs[i]] * xx + dx) + pixelAdjust;
84622 me[ohlcType](ctx, open, high, low, close, mid, halfWidth);
84623 }
84624 }
84625 ctx.fillStroke(template.attr);
84626 ctx.restore();
84627 }
84628 });
84629
84630 /**
84631 * @class Ext.chart.series.CandleStick
84632 * @extends Ext.chart.series.Cartesian
84633 *
84634 * Creates a candlestick or OHLC Chart.
84635 *
84636 * @example preview
84637 * var chart = new Ext.chart.CartesianChart({
84638 * animate: true,
84639 * store: {
84640 * fields: ['time', 'open', 'high', 'low', 'close'],
84641 * data: [
84642 * {'time':new Date('Jan 1 2010').getTime(), 'open':600, 'high':614, 'low':578, 'close':590},
84643 * {'time':new Date('Jan 2 2010').getTime(), 'open':590, 'high':609, 'low':580, 'close':580},
84644 * {'time':new Date('Jan 3 2010').getTime(), 'open':580, 'high':602, 'low':578, 'close':602},
84645 * {'time':new Date('Jan 4 2010').getTime(), 'open':602, 'high':614, 'low':586, 'close':586},
84646 * {'time':new Date('Jan 5 2010').getTime(), 'open':586, 'high':602, 'low':565, 'close':565}
84647 * ]
84648 * },
84649 * axes: [{
84650 * type: 'numeric',
84651 * position: 'left',
84652 * fields: ['open', 'high', 'low', 'close'],
84653 * title: {
84654 * text: 'Sample Values',
84655 * fontSize: 15
84656 * },
84657 * grid: true,
84658 * minimum: 560,
84659 * maximum: 640
84660 * }, {
84661 * type: 'time',
84662 * position: 'bottom',
84663 * fields: ['time'],
84664 * fromDate: new Date('Dec 31 2009'),
84665 * toDate: new Date('Jan 6 2010'),
84666 * title: {
84667 * text: 'Sample Values',
84668 * fontSize: 15
84669 * },
84670 * style: {
84671 * axisLine: false
84672 * }
84673 * }],
84674 * series: [{
84675 * type: 'candlestick',
84676 * xField: 'time',
84677 * openField: 'open',
84678 * highField: 'high',
84679 * lowField: 'low',
84680 * closeField: 'close',
84681 * style: {
84682 * dropStyle: {
84683 * fill: 'rgb(237, 123, 43)',
84684 * stroke: 'rgb(237, 123, 43)'
84685 * },
84686 * raiseStyle: {
84687 * fill: 'rgb(55, 153, 19)',
84688 * stroke: 'rgb(55, 153, 19)'
84689 * }
84690 * },
84691 * aggregator: {
84692 * strategy: 'time'
84693 * }
84694 * }]
84695 * });
84696 * Ext.Viewport.setLayout('fit');
84697 * Ext.Viewport.add(chart);
84698 */
84699 Ext.define('Ext.chart.series.CandleStick', {
84700 extend: Ext.chart.series.Cartesian ,
84701
84702 alias: 'series.candlestick',
84703 type: 'candlestick',
84704 seriesType: 'candlestickSeries',
84705 config: {
84706 /**
84707 * @cfg {String} openField
84708 * The store record field name that represents the opening value of the given period.
84709 */
84710 openField: null,
84711 /**
84712 * @cfg {String} highField
84713 * The store record field name that represents the highest value of the time interval represented.
84714 */
84715 highField: null,
84716 /**
84717 * @cfg {String} lowField
84718 * The store record field name that represents the lowest value of the time interval represented.
84719 */
84720 lowField: null,
84721 /**
84722 * @cfg {String} closeField
84723 * The store record field name that represents the closing value of the given period.
84724 */
84725 closeField: null
84726 },
84727
84728 fieldCategoryY: ['Open', 'High', 'Low', 'Close']
84729 });
84730
84731 /**
84732 * @class Ext.chart.series.Gauge
84733 * @extends Ext.chart.series.Series
84734 *
84735 * Creates a Gauge Chart.
84736 *
84737 * @example preview
84738 * var chart = new Ext.chart.SpaceFillingChart({
84739 * series: [{
84740 * type: 'gauge',
84741 * minimum: 100,
84742 * maximum: 800,
84743 * value: 400,
84744 * donut: 30,
84745 * colors: ["#115fa6", "lightgrey"]
84746 * }]
84747 * });
84748 * Ext.Viewport.setLayout('fit');
84749 * Ext.Viewport.add(chart);
84750 */
84751 Ext.define('Ext.chart.series.Gauge', {
84752 alias: 'series.gauge',
84753 extend: Ext.chart.series.Series ,
84754 type: "gauge",
84755 seriesType: 'pieslice',
84756
84757
84758
84759
84760
84761 config: {
84762 /**
84763 * @cfg {String} angleField
84764 * @deprecated Use `field` directly
84765 * The store record field name to be used for the gauge angles.
84766 * The values bound to this field name must be positive real numbers.
84767 */
84768 angleField: null,
84769
84770 /**
84771 * @cfg {String} field
84772 * The store record field name to be used for the gauge value.
84773 * The values bound to this field name must be positive real numbers.
84774 */
84775 field: null,
84776
84777 /**
84778 * @cfg {Boolean} needle
84779 * If true, display the gauge as a needle, otherwise as a sector.
84780 */
84781 needle: false,
84782
84783 /**
84784 * @cfg {Number} needleLengthRatio
84785 * @deprecated Use `needleLength` directly
84786 * Ratio of the length of needle compared to the radius of the entire disk.
84787 */
84788 needleLengthRatio: undefined,
84789
84790 /**
84791 * @cfg {Number} needleLength
84792 * Percentage of the length of needle compared to the radius of the entire disk.
84793 */
84794 needleLength: 90,
84795
84796 /**
84797 * @cfg {Number} needleWidth
84798 * Width of the needle in pixels.
84799 */
84800 needleWidth: 4,
84801
84802 /**
84803 * @cfg {Number} donut
84804 * Percentage of the radius of the donut hole compared to the entire disk.
84805 */
84806 donut: 30,
84807
84808 /**
84809 * @cfg {Boolean} showInLegend
84810 * Whether to add the gauge chart elements as legend items.
84811 */
84812 showInLegend: false,
84813
84814 /**
84815 * @cfg {Number} value
84816 * Directly sets the displayed value of the gauge.
84817 * It is ignored if {@link #field} is provided.
84818 */
84819 value: null,
84820
84821 /**
84822 * @cfg {Array} colors (required)
84823 * An array of color values which is used for the needle and the `sectors`.
84824 */
84825 colors: null,
84826
84827 /**
84828 * @cfg {Array} sectors
84829 * Allows to paint sectors of different colors in the background of the gauge,
84830 * with optional labels.
84831 *
84832 * It can be an array of numbers (each between `minimum` and `maximum`) that
84833 * define the highest value of each sector. For N sectors, only (N-1) values are
84834 * needed because it is assumed that the first sector starts at `minimum` and the
84835 * last sector ends at `maximum`. Example: a water temperature gauge that is blue
84836 * below 20C, red above 80C, gray in-between, and with an orange needle...
84837 *
84838 * minimum: 0,
84839 * maximum: 100,
84840 * sectors: [20, 80],
84841 * colors: ['orange', 'blue', 'lightgray', 'red']
84842 *
84843 * It can be also an array of objects, each with the following properties:
84844 *
84845 * @cfg {Number} sectors.start The starting value of the sector. If omitted, it
84846 * uses the previous sector's `end` value or the chart's `minimum`.
84847 * @cfg {Number} sectors.end The ending value of the sector. If omitted, it uses
84848 * the `maximum` defined for the chart.
84849 * @cfg {String} sectors.label The label for this sector. Labels are styled using
84850 * the series' {@link Ext.chart.series.Series#label label} config.
84851 * @cfg {String} sectors.color The color of the sector. If omitted, it uses one
84852 * of the `colors` defined for the series or for the chart.
84853 * @cfg {Object} sectors.style An additional style object for the sector (for
84854 * instance to set the opacity or to draw a line of a different color around the
84855 * sector).
84856 *
84857 * minimum: 0,
84858 * maximum: 100,
84859 * sectors: [{
84860 * end: 20,
84861 * label: 'Cold',
84862 * color: 'aqua'
84863 * },
84864 * {
84865 * end: 80,
84866 * label: 'Temp.',
84867 * color: 'lightgray',
84868 * style: { strokeStyle:'black', strokeOpacity:1, lineWidth:1 }
84869 * },
84870 * {
84871 * label: 'Hot',
84872 * color: 'tomato'
84873 * }]
84874 */
84875 sectors: null,
84876
84877 /**
84878 * @cfg {Number} minimum
84879 * The minimum value of the gauge.
84880 */
84881 minimum: 0,
84882
84883 /**
84884 * @cfg {Number} maximum
84885 * The maximum value of the gauge.
84886 */
84887 maximum: 100,
84888
84889 rotation: 0,
84890
84891 /**
84892 * @cfg {Number} totalAngle
84893 * The size of the sector that the series will occupy.
84894 */
84895 totalAngle: Math.PI / 2,
84896
84897 region: [0, 0, 1, 1],
84898
84899 center: [0.5, 0.75],
84900
84901 radius: 0.5,
84902
84903 /**
84904 * @cfg {Boolean} wholeDisk Indicates whether to show the whole disk or only the marked part.
84905 */
84906 wholeDisk: false
84907 },
84908
84909 updateNeedle: function(needle) {
84910 var me = this,
84911 sprites = me.getSprites(),
84912 angle = me.valueToAngle(me.getValue());
84913
84914 if (sprites && sprites.length) {
84915 sprites[0].setAttributes({
84916 startAngle: (needle ? angle : 0),
84917 endAngle: angle,
84918 strokeOpacity: (needle ? 1 : 0),
84919 lineWidth: (needle ? me.getNeedleWidth() : 0)
84920
84921 });
84922 me.doUpdateStyles();
84923 }
84924 },
84925
84926 updateColors: function (colors, oldColors) {
84927 var me = this,
84928 sectors = me.getSectors(),
84929 sectorCount = sectors && sectors.length,
84930 sprites = me.getSprites(),
84931 spriteCount = sprites && sprites.length,
84932 newColors = Ext.Array.clone(colors),
84933 colorCount = colors && colors.length,
84934 needle = me.getNeedle(),
84935 i;
84936
84937 if (!colorCount || !colors[0]) {
84938 return;
84939 }
84940
84941 // Make sure the 'sectors' colors are not overridden.
84942 for (i = 0; i < sectorCount; i++) {
84943 newColors[i+1] = sectors[i].color || newColors[i+1] || colors[i%colorCount];
84944 }
84945
84946 sprites[0].setAttributes({stroke:newColors[0]});
84947 this.setSubStyle({color:newColors});
84948 this.doUpdateStyles();
84949 },
84950
84951 updateAngleField: function (angleField) {
84952 this.setField(angleField);
84953 },
84954
84955 updateNeedleLengthRatio: function (needleLengthRatio) {
84956 this.setNeedleLength(needleLengthRatio * 100);
84957 },
84958
84959 updateRegion: function (region) {
84960 var wholeDisk = this.getWholeDisk(),
84961 halfTotalAngle = wholeDisk ? Math.PI : this.getTotalAngle() / 2,
84962 donut = this.getDonut() / 100,
84963 width, height, radius;
84964 if (halfTotalAngle <= Math.PI / 2) {
84965 width = 2 * Math.sin(halfTotalAngle);
84966 height = 1 - donut * Math.cos(halfTotalAngle);
84967 } else {
84968 width = 2;
84969 height = 1 - Math.cos(halfTotalAngle);
84970 }
84971
84972 radius = Math.min(region[2] / width, region[3] / height);
84973 this.setRadius(radius);
84974 this.setCenter([region[2] / 2, radius + (region[3] - height * radius) / 2]);
84975 },
84976
84977 updateCenter: function (center) {
84978 this.setStyle({
84979 centerX: center[0],
84980 centerY: center[1],
84981 rotationCenterX: center[0],
84982 rotationCenterY: center[1]
84983 });
84984 this.doUpdateStyles();
84985 },
84986
84987 updateRotation: function (rotation) {
84988 this.setStyle({
84989 rotationRads: rotation - (this.getTotalAngle() + Math.PI) / 2
84990 });
84991 this.doUpdateStyles();
84992 },
84993
84994 doUpdateShape: function (radius, donut) {
84995 var endRhoArray,
84996 sectors = this.getSectors(),
84997 sectorCount = (sectors && sectors.length) || 0,
84998 needleLength = this.getNeedleLength() / 100;
84999
85000 // Initialize an array that contains the endRho for each sprite.
85001 // The first sprite is for the needle, the others for the gauge background sectors.
85002 // Note: SubStyle arrays are handled in series.getOverriddenStyleByIndex().
85003 endRhoArray = [radius * needleLength, radius];
85004 while (sectorCount --) {
85005 endRhoArray.push(radius);
85006 }
85007
85008 this.setSubStyle({
85009 endRho: endRhoArray,
85010 startRho: radius / 100 * donut
85011 });
85012 this.doUpdateStyles();
85013 },
85014
85015 updateRadius: function (radius) {
85016 var donut = this.getDonut();
85017 this.doUpdateShape(radius, donut);
85018 },
85019
85020 updateDonut: function (donut) {
85021 var radius = this.getRadius();
85022 this.doUpdateShape(radius, donut);
85023 },
85024
85025 valueToAngle: function(value) {
85026 value = this.applyValue(value);
85027 return this.getTotalAngle() * (value - this.getMinimum()) / (this.getMaximum() - this.getMinimum());
85028 },
85029
85030 applyValue: function (value) {
85031 return Math.min(this.getMaximum(), Math.max(value, this.getMinimum()));
85032 },
85033
85034 updateValue: function (value) {
85035 var me = this,
85036 needle = me.getNeedle(),
85037 angle = me.valueToAngle(value),
85038 sprites = me.getSprites();
85039
85040 sprites[0].rendererData.value = value;
85041 sprites[0].setAttributes({
85042 startAngle: (needle ? angle : 0),
85043 endAngle: angle
85044 });
85045 me.doUpdateStyles();
85046 },
85047
85048 processData: function () {
85049 var store = this.getStore();
85050 if (!store) {
85051 return;
85052 }
85053 var field = this.getField();
85054 if (!field) {
85055 return;
85056 }
85057 if (!store.getData().items.length) {
85058 return;
85059 }
85060 this.setValue(store.getData().items[0].get(field));
85061 },
85062
85063 getDefaultSpriteConfig: function () {
85064 return {
85065 type: this.seriesType,
85066 renderer: this.getRenderer(),
85067 fx: {
85068 customDuration: {
85069 translationX: 0,
85070 translationY: 0,
85071 rotationCenterX: 0,
85072 rotationCenterY: 0,
85073 centerX: 0,
85074 centerY: 0,
85075 startRho: 0,
85076 endRho: 0,
85077 baseRotation: 0
85078 }
85079 }
85080 };
85081 },
85082
85083 normalizeSectors: function(sectors) {
85084 // Make sure all the sectors in the array have a legit start and end.
85085 // Note: the array is modified in-place.
85086 var me = this,
85087 sectorCount = (sectors && sectors.length) || 0,
85088 i, value, start, end;
85089
85090 if (sectorCount) {
85091 for (i = 0; i < sectorCount; i++) {
85092 value = sectors[i];
85093 if (typeof value == "number") {
85094 sectors[i] = {
85095 start: (i > 0 ? sectors[i-1].end : me.getMinimum()),
85096 end: Math.min(value, me.getMaximum())
85097 };
85098 if (i == (sectorCount - 1) && sectors[i].end < me.getMaximum()) {
85099 sectors[i+1] = {
85100 start: sectors[i].end,
85101 end: me.getMaximum()
85102 };
85103 }
85104 } else {
85105 if (typeof value.start == "number") {
85106 start = Math.max(value.start, me.getMinimum());
85107 } else {
85108 start = (i > 0 ? sectors[i-1].end : me.getMinimum());
85109 }
85110 if (typeof value.end == "number") {
85111 end = Math.min(value.end, me.getMaximum());
85112 } else {
85113 end = me.getMaximum();
85114 }
85115 sectors[i].start = start;
85116 sectors[i].end = end;
85117 }
85118 }
85119 } else {
85120 sectors = [{
85121 start: me.getMinimum(),
85122 end: me.getMaximum()
85123 }];
85124 }
85125 return sectors;
85126 },
85127
85128 getSprites: function () {
85129 var me = this,
85130 store = me.getStore(),
85131 value = me.getValue(),
85132 i, ln;
85133
85134 // The store must be initialized, or the value must be set
85135 if (!store && !Ext.isNumber(value)) {
85136 return [];
85137 }
85138
85139 // Return cached sprites
85140 var chart = me.getChart(),
85141 animate = chart.getAnimate(),
85142 sprites = me.sprites,
85143 spriteIndex = 0,
85144 sprite, sectors, attr, rendererData;
85145
85146 if (sprites && sprites.length) {
85147 sprites[0].fx.setConfig(animate);
85148 return sprites;
85149 }
85150
85151 rendererData = {
85152 store: store,
85153 field: me.getField(),
85154 value: value,
85155 series: me
85156 };
85157
85158 // Create needle sprite
85159 sprite = me.createSprite();
85160 sprite.setAttributes({
85161 zIndex: 10
85162 }, true);
85163 sprite.rendererData = rendererData;
85164 sprite.rendererIndex = spriteIndex++;
85165
85166 // Create background sprite(s)
85167 me.getLabel().getTemplate().setField(true); // Enable labels
85168 sectors = me.normalizeSectors(me.getSectors());
85169 for (i = 0, ln = sectors.length; i < ln; i++) {
85170 attr = {
85171 startAngle: me.valueToAngle(sectors[i].start),
85172 endAngle: me.valueToAngle(sectors[i].end),
85173 label: sectors[i].label,
85174 fillStyle: sectors[i].color,
85175 strokeOpacity: 0,
85176 rotateLabels: false,
85177 doCallout: false, // Show labels inside sectors.
85178 labelOverflowPadding: -1 // Allow labels to overlap.
85179 };
85180 Ext.apply(attr, sectors[i].style);
85181 sprite = me.createSprite();
85182 sprite.rendererData = rendererData;
85183 sprite.rendererIndex = spriteIndex++;
85184 sprite.setAttributes(attr, true);
85185 }
85186
85187 // Make sure we have some default colors
85188 var colors = me.getColors() || (chart && chart.config.colors);
85189 if (!colors) {
85190 me.setColors(['blue','lightgray']);
85191 }
85192
85193 me.doUpdateStyles();
85194 return sprites;
85195 }
85196 });
85197
85198
85199 /**
85200 * @private
85201 */
85202 Ext.define('Ext.event.publisher.Publisher', {
85203 targetType: '',
85204
85205 idSelectorRegex: /^#([\w\-]+)$/i,
85206
85207 constructor: function() {
85208 var handledEvents = this.handledEvents,
85209 handledEventsMap,
85210 i, ln, event;
85211
85212 handledEventsMap = this.handledEventsMap = {};
85213
85214 for (i = 0,ln = handledEvents.length; i < ln; i++) {
85215 event = handledEvents[i];
85216
85217 handledEventsMap[event] = true;
85218 }
85219
85220 this.subscribers = {};
85221
85222 return this;
85223 },
85224
85225 handles: function(eventName) {
85226 var map = this.handledEventsMap;
85227
85228 return !!map[eventName] || !!map['*'] || eventName === '*';
85229 },
85230
85231 getHandledEvents: function() {
85232 return this.handledEvents;
85233 },
85234
85235 setDispatcher: function(dispatcher) {
85236 this.dispatcher = dispatcher;
85237 },
85238
85239 subscribe: function() {
85240 return false;
85241 },
85242
85243 unsubscribe: function() {
85244 return false;
85245 },
85246
85247 unsubscribeAll: function() {
85248 delete this.subscribers;
85249 this.subscribers = {};
85250
85251 return this;
85252 },
85253
85254 notify: function() {
85255 return false;
85256 },
85257
85258 getTargetType: function() {
85259 return this.targetType;
85260 },
85261
85262 dispatch: function(target, eventName, args) {
85263 this.dispatcher.doDispatchEvent(this.targetType, target, eventName, args);
85264 }
85265 });
85266
85267 /**
85268 * @private
85269 */
85270 Ext.define('Ext.chart.series.ItemPublisher', {
85271 extend: Ext.event.publisher.Publisher ,
85272
85273 targetType: 'series',
85274
85275 handledEvents: [
85276 /**
85277 * @event itemmousemove
85278 * Fires when the mouse is moved on a series item.
85279 * @param {Ext.chart.series.Series} series
85280 * @param {Object} item
85281 * @param {Event} event
85282 */
85283 'itemmousemove',
85284 /**
85285 * @event itemmouseup
85286 * Fires when a mouseup event occurs on a series item.
85287 * @param {Ext.chart.series.Series} series
85288 * @param {Object} item
85289 * @param {Event} event
85290 */
85291 'itemmouseup',
85292 /**
85293 * @event itemmousedown
85294 * Fires when a mousedown event occurs on a series item.
85295 * @param {Ext.chart.series.Series} series
85296 * @param {Object} item
85297 * @param {Event} event
85298 */
85299 'itemmousedown',
85300 /**
85301 * @event itemmouseover
85302 * Fires when the mouse enters a series item.
85303 * @param {Ext.chart.series.Series} series
85304 * @param {Object} item
85305 * @param {Event} event
85306 */
85307 'itemmouseover',
85308 /**
85309 * @event itemmouseout
85310 * Fires when the mouse exits a series item.
85311 * @param {Ext.chart.series.Series} series
85312 * @param {Object} item
85313 * @param {Event} event
85314 */
85315 'itemmouseout',
85316 /**
85317 * @event itemclick
85318 * Fires when a click event occurs on a series item.
85319 * @param {Ext.chart.series.Series} series
85320 * @param {Object} item
85321 * @param {Event} event
85322 */
85323 'itemclick',
85324 /**
85325 * @event itemdoubleclick
85326 * Fires when a doubleclick event occurs on a series item.
85327 * @param {Ext.chart.series.Series} series
85328 * @param {Object} item
85329 * @param {Event} event
85330 */
85331 'itemdoubleclick',
85332 /**
85333 * @event itemtap
85334 * Fires when a tap event occurs on a series item.
85335 * @param {Ext.chart.series.Series} series
85336 * @param {Object} item
85337 * @param {Event} event
85338 */
85339 'itemtap',
85340 /**
85341 * @event itemtapstart
85342 * Fires when a tapstart event occurs on a series item.
85343 * @param {Ext.chart.series.Series} series
85344 * @param {Object} item
85345 * @param {Event} event
85346 */
85347 'itemtapstart',
85348 /**
85349 * @event itemtapend
85350 * Fires when a tapend event occurs on a series item.
85351 * @param {Ext.chart.series.Series} series
85352 * @param {Object} item
85353 * @param {Event} event
85354 */
85355 'itemtapend',
85356 /**
85357 * @event itemtapcancel
85358 * Fires when a tapcancel event occurs on a series item.
85359 * @param {Ext.chart.series.Series} series
85360 * @param {Object} item
85361 * @param {Event} event
85362 */
85363 'itemtapcancel',
85364 /**
85365 * @event itemtaphold
85366 * Fires when a taphold event occurs on a series item.
85367 * @param {Ext.chart.series.Series} series
85368 * @param {Object} item
85369 * @param {Event} event
85370 */
85371 'itemtaphold',
85372 /**
85373 * @event itemdoubletap
85374 * Fires when a doubletap event occurs on a series item.
85375 * @param {Ext.chart.series.Series} series
85376 * @param {Object} item
85377 * @param {Event} event
85378 */
85379 'itemdoubletap',
85380 /**
85381 * @event itemsingletap
85382 * Fires when a singletap event occurs on a series item.
85383 * @param {Ext.chart.series.Series} series
85384 * @param {Object} item
85385 * @param {Event} event
85386 */
85387 'itemsingletap',
85388 /**
85389 * @event itemtouchstart
85390 * Fires when a touchstart event occurs on a series item.
85391 * @param {Ext.chart.series.Series} series
85392 * @param {Object} item
85393 * @param {Event} event
85394 */
85395 'itemtouchstart',
85396 /**
85397 * @event itemtouchmove
85398 * Fires when a touchmove event occurs on a series item.
85399 * @param {Ext.chart.series.Series} series
85400 * @param {Object} item
85401 * @param {Event} event
85402 */
85403 'itemtouchmove',
85404 /**
85405 * @event itemtouchend
85406 * Fires when a touchend event occurs on a series item.
85407 * @param {Ext.chart.series.Series} series
85408 * @param {Object} item
85409 * @param {Event} event
85410 */
85411 'itemtouchend',
85412 /**
85413 * @event itemdragstart
85414 * Fires when a dragstart event occurs on a series item.
85415 * @param {Ext.chart.series.Series} series
85416 * @param {Object} item
85417 * @param {Event} event
85418 */
85419 'itemdragstart',
85420 /**
85421 * @event itemdrag
85422 * Fires when a drag event occurs on a series item.
85423 * @param {Ext.chart.series.Series} series
85424 * @param {Object} item
85425 * @param {Event} event
85426 */
85427 'itemdrag',
85428 /**
85429 * @event itemdragend
85430 * Fires when a dragend event occurs on a series item.
85431 * @param {Ext.chart.series.Series} series
85432 * @param {Object} item
85433 * @param {Event} event
85434 */
85435 'itemdragend',
85436 /**
85437 * @event itempinchstart
85438 * Fires when a pinchstart event occurs on a series item.
85439 * @param {Ext.chart.series.Series} series
85440 * @param {Object} item
85441 * @param {Event} event
85442 */
85443 'itempinchstart',
85444 /**
85445 * @event itempinch
85446 * Fires when a pinch event occurs on a series item.
85447 * @param {Ext.chart.series.Series} series
85448 * @param {Object} item
85449 * @param {Event} event
85450 */
85451 'itempinch',
85452 /**
85453 * @event itempinchend
85454 * Fires when a pinchend event occurs on a series item.
85455 * @param {Ext.chart.series.Series} series
85456 * @param {Object} item
85457 * @param {Event} event
85458 */
85459 'itempinchend',
85460 /**
85461 * @event itemswipe
85462 * Fires when a swipe event occurs on a series item.
85463 * @param {Ext.chart.series.Series} series
85464 * @param {Object} item
85465 * @param {Event} event
85466 */
85467 'itemswipe'
85468 ],
85469
85470 delegationRegex: /^item([a-z]+)$/i,
85471
85472 getSubscribers: function (chartId) {
85473 var subscribers = this.subscribers;
85474
85475 if (!subscribers.hasOwnProperty(chartId)) {
85476 subscribers[chartId] = {};
85477 }
85478
85479 return subscribers[chartId];
85480 },
85481
85482 subscribe: function (target, eventName) {
85483 var match = target.match(this.idSelectorRegex),
85484 dispatcher = this.dispatcher,
85485 targetType = this.targetType,
85486 series, id;
85487
85488 if (!match) {
85489 return false;
85490 }
85491
85492 id = match[1];
85493 series = Ext.ComponentManager.get(id);
85494 if (!series) {
85495 return false;
85496 }
85497
85498 if (!series.getChart()) {
85499 dispatcher.addListener(targetType, target, 'chartattached', 'attachChart', this, [series, eventName], 'before');
85500 } else {
85501 this.attachChart(series.getChart(), [series, eventName]);
85502 }
85503
85504 return true;
85505 },
85506
85507 attachChart: function (chart, args) {
85508 var dispatcher = this.dispatcher,
85509 targetType = this.targetType,
85510 series = args[0],
85511 eventName = args[1],
85512 subscribers = this.getSubscribers(chart.getId()),
85513 match = eventName.match(this.delegationRegex);
85514 if (match) {
85515 var chartEventName = match[1];
85516 if (!subscribers.hasOwnProperty(eventName)) {
85517 subscribers[eventName] = [];
85518 dispatcher.addListener(targetType, '#' + series.getId(), 'chartdetached', 'detachChart', this, [series, eventName, subscribers], 'after');
85519 chart.element.on(chartEventName, "relayMethod", this, [chart, eventName]);
85520 }
85521 subscribers[eventName].push(series);
85522 return true;
85523 } else {
85524 return false;
85525 }
85526 },
85527
85528 unsubscribe: function (target, eventName) {
85529 var match = target.match(this.idSelectorRegex),
85530 dispatcher = this.dispatcher,
85531 targetType = this.targetType,
85532 series, id;
85533
85534 if (!match) {
85535 return false;
85536 }
85537
85538 id = match[1];
85539 series = Ext.ComponentManager.get(id);
85540 if (!series) {
85541 return false;
85542 }
85543
85544 dispatcher.removeListener(targetType, target, 'chartattached', 'attachChart', this, 'before');
85545 if (series.getChart()) {
85546 this.detachChart(series.getChart(), [series, eventName]);
85547 }
85548 return true;
85549 },
85550
85551 detachChart: function (chart, args) {
85552 var dispatcher = this.dispatcher,
85553 targetType = this.targetType,
85554 series = args[0],
85555 eventName = args[1],
85556 subscribers = this.getSubscribers(chart.getId()),
85557 match = eventName.match(this.delegationRegex),
85558 index, seriesArray;
85559 if (match) {
85560 var chartEventName = match[1];
85561 if (subscribers.hasOwnProperty(eventName)) {
85562 seriesArray = subscribers[eventName];
85563 index = seriesArray.indexOf(series);
85564 if (index > -1) {
85565 seriesArray.splice(index, 1);
85566 }
85567 if (seriesArray.length === 0) {
85568 chart.element.un(chartEventName, "relayMethod", this, [chart, eventName]);
85569 dispatcher.removeListener(targetType, '#' + series.getId(), 'chartdetached', 'detachChart', this, 'after');
85570 delete subscribers[eventName];
85571 }
85572 }
85573 }
85574 },
85575
85576 relayMethod: function (e, sender, args) {
85577 var chart = args[0],
85578 eventName = args[1],
85579 dispatcher = this.dispatcher,
85580 targetType = this.targetType,
85581 chartXY = chart.getEventXY(e),
85582 x = chartXY[0],
85583 y = chartXY[1],
85584 subscriber = this.getSubscribers(chart.getId())[eventName],
85585 i, ln;
85586 if (subscriber) {
85587 for (i = 0, ln = subscriber.length; i < ln; i++) {
85588 var series = subscriber[i],
85589 item = series.getItemForPoint(x, y);
85590 if (item) {
85591 // TODO: Don't stop at the first item.
85592 // Depending on the selectionTolerance, there might be an item in another
85593 // series that's closer to the event location. See test case 3943c.
85594 dispatcher.doDispatchEvent(targetType, '#' + series.getId(), eventName, [series, item, e]);
85595 return;
85596 }
85597 }
85598 }
85599 }
85600
85601 }, function () {
85602
85603 });
85604
85605 /**
85606 * @class Ext.chart.series.sprite.Line
85607 * @extends Ext.chart.series.sprite.Aggregative
85608 *
85609 * Line series sprite.
85610 */
85611 Ext.define('Ext.chart.series.sprite.Line', {
85612 alias: 'sprite.lineSeries',
85613 extend: Ext.chart.series.sprite.Aggregative ,
85614
85615 inheritableStatics: {
85616 def: {
85617 processors: {
85618 smooth: 'bool',
85619 fillArea: 'bool',
85620 step: 'bool',
85621 preciseStroke: 'bool'
85622 },
85623
85624 defaults: {
85625 /**
85626 * @cfg {Boolean} smooth 'true' if the sprite uses line smoothing.
85627 */
85628 smooth: false,
85629
85630 /**
85631 * @cfg {Boolean} fillArea 'true' if the sprite paints the area underneath the line.
85632 */
85633 fillArea: false,
85634
85635 /**
85636 * @cfg {Boolean} step 'true' if the line uses steps instead of straight lines to connect the dots.
85637 * It is ignored if `smooth` is true.
85638 */
85639 step: false,
85640
85641 /**
85642 * @cfg {Boolean} preciseStroke 'true' if the line uses precise stroke.
85643 */
85644 preciseStroke: true
85645 },
85646
85647 dirtyTriggers: {
85648 dataX: 'dataX,bbox,smooth',
85649 dataY: 'dataY,bbox,smooth',
85650 smooth: 'smooth'
85651 },
85652
85653 updaters: {
85654 smooth: function (attr) {
85655 if (attr.smooth && attr.dataX && attr.dataY && attr.dataX.length > 2 && attr.dataY.length > 2) {
85656 this.smoothX = Ext.draw.Draw.spline(attr.dataX);
85657 this.smoothY = Ext.draw.Draw.spline(attr.dataY);
85658 } else {
85659 delete this.smoothX;
85660 delete this.smoothY;
85661 }
85662 }
85663 }
85664 }
85665 },
85666
85667 list: null,
85668
85669 updatePlainBBox: function (plain) {
85670 var attr = this.attr,
85671 ymin = Math.min(0, attr.dataMinY),
85672 ymax = Math.max(0, attr.dataMaxY);
85673 plain.x = attr.dataMinX;
85674 plain.y = ymin;
85675 plain.width = attr.dataMaxX - attr.dataMinX;
85676 plain.height = ymax - ymin;
85677 },
85678
85679 drawStroke: function (surface, ctx, start, end, list, xAxis) {
85680 var attr = this.attr,
85681 matrix = attr.matrix,
85682 xx = matrix.getXX(),
85683 yy = matrix.getYY(),
85684 dx = matrix.getDX(),
85685 dy = matrix.getDY(),
85686 smooth = attr.smooth,
85687 step = attr.step,
85688 scale = Math.pow(2, power(attr.dataX.length, end)),
85689 smoothX = this.smoothX,
85690 smoothY = this.smoothY,
85691 i, j, lineConfig, changes,
85692 cx1, cy1, cx2, cy2, x, y, x0, y0, saveOpacity;
85693
85694 function power(count, end) {
85695 var power = 0,
85696 n = count;
85697 while (n > 0 && n < end) {
85698 power++;
85699 n += count >> power;
85700 }
85701 return power > 0 ? power - 1 : power;
85702 }
85703
85704 ctx.beginPath();
85705 if (smooth && smoothX && smoothY) {
85706 ctx.moveTo(smoothX[start * 3] * xx + dx, smoothY[start * 3] * yy + dy);
85707 for (i = 0, j = start * 3 + 1; i < list.length - 3; i += 3, j += 3 * scale) {
85708 cx1 = smoothX[j] * xx + dx;
85709 cy1 = smoothY[j] * yy + dy;
85710 cx2 = smoothX[j + 1] * xx + dx;
85711 cy2 = smoothY[j + 1] * yy + dy;
85712 x = list[i + 3];
85713 y = list[i + 4];
85714 x0 = list[i];
85715 y0 = list[i + 1];
85716 if (attr.renderer) {
85717 lineConfig = {
85718 type: 'line',
85719 smooth: true,
85720 step: step,
85721 cx1: cx1,
85722 cy1: cy1,
85723 cx2: cx2,
85724 cy2: cy2,
85725 x: x,
85726 y: y,
85727 x0: x0,
85728 y0: y0
85729 };
85730 changes = attr.renderer.call(this, this, lineConfig, {store:this.getStore()}, (i/3 + 1));
85731 ctx.save();
85732 Ext.apply(ctx, changes);
85733 // Fill the area if we need to, using the fill color and transparent strokes.
85734 if (attr.fillArea) {
85735 saveOpacity = ctx.strokeOpacity;
85736 ctx.save();
85737 ctx.strokeOpacity = 0;
85738 ctx.moveTo(x0, y0);
85739 ctx.bezierCurveTo(cx1, cy1, cx2, cy2, x, y);
85740 ctx.lineTo(x, xAxis);
85741 ctx.lineTo(x0, xAxis);
85742 ctx.lineTo(x0, y0);
85743 ctx.closePath();
85744 ctx.fillStroke(attr, true);
85745 ctx.restore();
85746 ctx.strokeOpacity = saveOpacity;
85747 ctx.beginPath();
85748 }
85749 // Draw the line on top of the filled area.
85750 ctx.moveTo(x0, y0);
85751 ctx.bezierCurveTo(cx1, cy1, cx2, cy2, x, y);
85752 ctx.moveTo(x0, y0);
85753 ctx.closePath();
85754 ctx.stroke();
85755 ctx.restore();
85756 ctx.beginPath();
85757 ctx.moveTo(x, y);
85758 } else {
85759 ctx.bezierCurveTo(cx1, cy1, cx2, cy2, x, y);
85760 }
85761 }
85762 } else {
85763 ctx.moveTo(list[0], list[1]);
85764 for (i = 3; i < list.length; i += 3) {
85765 x = list[i];
85766 y = list[i + 1];
85767 x0 = list[i - 3];
85768 y0 = list[i - 2];
85769 if (attr.renderer) {
85770 lineConfig = {
85771 type: 'line',
85772 smooth: false,
85773 step: step,
85774 x: x,
85775 y: y,
85776 x0: x0,
85777 y0: y0
85778 };
85779 changes = attr.renderer.call(this, this, lineConfig, {store:this.getStore()}, i/3);
85780 ctx.save();
85781 Ext.apply(ctx, changes);
85782 // Fill the area if we need to, using the fill color and transparent strokes.
85783 if (attr.fillArea) {
85784 saveOpacity = ctx.strokeOpacity;
85785 ctx.save();
85786 ctx.strokeOpacity = 0;
85787 if (step) {
85788 ctx.lineTo(x, y0);
85789 } else {
85790 ctx.lineTo(x, y);
85791 }
85792 ctx.lineTo(x, xAxis);
85793 ctx.lineTo(x0, xAxis);
85794 ctx.lineTo(x0, y0);
85795 ctx.closePath();
85796 ctx.fillStroke(attr, true);
85797 ctx.restore();
85798 ctx.strokeOpacity = saveOpacity;
85799 ctx.beginPath();
85800 }
85801 // Draw the line (or the 2 lines if 'step') on top of the filled area.
85802 ctx.moveTo(x0, y0);
85803 if (step) {
85804 ctx.lineTo(x, y0);
85805 ctx.closePath();
85806 ctx.stroke();
85807 ctx.beginPath();
85808 ctx.moveTo(x, y0);
85809 }
85810 ctx.lineTo(x, y);
85811 ctx.closePath();
85812 ctx.stroke();
85813 ctx.restore();
85814 ctx.beginPath();
85815 ctx.moveTo(x, y);
85816 } else {
85817 if (step) {
85818 ctx.lineTo(x, y0);
85819 }
85820 ctx.lineTo(x, y);
85821 }
85822 }
85823 }
85824 },
85825
85826 drawLabel: function (text, dataX, dataY, labelId, region) {
85827 var me = this,
85828 attr = me.attr,
85829 label = me.getBoundMarker('labels')[0],
85830 labelTpl = label.getTemplate(),
85831 labelCfg = me.labelCfg || (me.labelCfg = {}),
85832 surfaceMatrix = me.surfaceMatrix,
85833 labelX, labelY,
85834 labelOverflowPadding = attr.labelOverflowPadding,
85835 halfWidth, halfHeight,
85836 labelBox,
85837 changes;
85838
85839 labelCfg.text = text;
85840
85841 labelBox = this.getMarkerBBox('labels', labelId, true);
85842 if (!labelBox) {
85843 me.putMarker('labels', labelCfg, labelId);
85844 labelBox = this.getMarkerBBox('labels', labelId, true);
85845 }
85846
85847 if (attr.flipXY) {
85848 labelCfg.rotationRads = Math.PI * 0.5;
85849 } else {
85850 labelCfg.rotationRads = 0;
85851 }
85852
85853 halfWidth = labelBox.width / 2;
85854 halfHeight = labelBox.height / 2;
85855
85856 labelX = dataX;
85857 if (labelTpl.attr.display === 'over') {
85858 labelY = dataY + halfHeight + labelOverflowPadding;
85859 } else {
85860 labelY = dataY - halfHeight - labelOverflowPadding;
85861 }
85862
85863 if (labelX <= region[0] + halfWidth) {
85864 labelX = region[0] + halfWidth;
85865 } else if (labelX >= region[2] - halfWidth) {
85866 labelX = region[2] - halfWidth;
85867 }
85868
85869 if (labelY <= region[1] + halfHeight) {
85870 labelY = region[1] + halfHeight;
85871 } else if (labelY >= region[3] - halfHeight) {
85872 labelY = region[3] - halfHeight;
85873 }
85874
85875 labelCfg.x = surfaceMatrix.x(labelX, labelY);
85876 labelCfg.y = surfaceMatrix.y(labelX, labelY);
85877
85878 if (labelTpl.attr.renderer) {
85879 changes = labelTpl.attr.renderer.call(this, text, label, labelCfg, {store: this.getStore()}, labelId);
85880 if (typeof changes === 'string') {
85881 labelCfg.text = changes;
85882 } else {
85883 Ext.apply(labelCfg, changes);
85884 }
85885 }
85886
85887 me.putMarker('labels', labelCfg, labelId);
85888 },
85889
85890 renderAggregates: function (aggregates, start, end, surface, ctx, clip, region) {
85891 var me = this,
85892 attr = me.attr,
85893 dataX = attr.dataX,
85894 dataY = attr.dataY,
85895 labels = attr.labels,
85896 drawLabels = labels && !!me.getBoundMarker('labels'),
85897 matrix = attr.matrix,
85898 surfaceMatrix = surface.matrix,
85899 pixel = surface.devicePixelRatio,
85900 xx = matrix.getXX(),
85901 yy = matrix.getYY(),
85902 dx = matrix.getDX(),
85903 dy = matrix.getDY(),
85904 markerCfg = {},
85905 list = this.list || (this.list = []),
85906 x, y, i, index,
85907 minXs = aggregates.minX,
85908 maxXs = aggregates.maxX,
85909 minYs = aggregates.minY,
85910 maxYs = aggregates.maxY,
85911 idx = aggregates.startIdx;
85912
85913 list.length = 0;
85914 for (i = start; i < end; i++) {
85915 var minX = minXs[i],
85916 maxX = maxXs[i],
85917 minY = minYs[i],
85918 maxY = maxYs[i];
85919
85920 if (minX < maxX) {
85921 list.push(minX * xx + dx, minY * yy + dy, idx[i]);
85922 list.push(maxX * xx + dx, maxY * yy + dy, idx[i]);
85923 } else if (minX > maxX) {
85924 list.push(maxX * xx + dx, maxY * yy + dy, idx[i]);
85925 list.push(minX * xx + dx, minY * yy + dy, idx[i]);
85926 } else {
85927 list.push(maxX * xx + dx, maxY * yy + dy, idx[i]);
85928 }
85929 }
85930
85931 if (list.length) {
85932 for (i = 0; i < list.length; i += 3) {
85933 x = list[i];
85934 y = list[i + 1];
85935 index = list[i + 2];
85936 if (attr.renderer) {
85937 markerCfg = {
85938 type: 'marker',
85939 x: x,
85940 y: y
85941 };
85942 markerCfg = attr.renderer.call(this, this, markerCfg, {store:this.getStore()}, i/3) || {};
85943 }
85944 markerCfg.translationX = surfaceMatrix.x(x, y);
85945 markerCfg.translationY = surfaceMatrix.y(x, y);
85946 me.putMarker('markers', markerCfg, index, !attr.renderer);
85947
85948 if (drawLabels && labels[index]) {
85949 me.drawLabel(labels[index], x, y, index, region);
85950 }
85951 }
85952 me.drawStroke(surface, ctx, start, end, list, region[1] - pixel);
85953 if (!attr.renderer) {
85954 var lastPointX = dataX[dataX.length - 1] * xx + dx + pixel,
85955 lastPointY = dataY[dataY.length - 1] * yy + dy,
85956 bottomY = region[1] - pixel,
85957 firstPointX = dataX[0] * xx + dx - pixel,
85958 firstPointY = dataY[0] * yy + dy;
85959 ctx.lineTo(lastPointX, lastPointY);
85960 ctx.lineTo(lastPointX, bottomY);
85961 ctx.lineTo(firstPointX, bottomY);
85962 ctx.lineTo(firstPointX, firstPointY);
85963 }
85964 ctx.closePath();
85965
85966 if (attr.transformFillStroke) {
85967 attr.matrix.toContext(ctx);
85968 }
85969 if (attr.preciseStroke) {
85970 if (attr.fillArea) {
85971 ctx.fill();
85972 }
85973 if (attr.transformFillStroke) {
85974 attr.inverseMatrix.toContext(ctx);
85975 }
85976 me.drawStroke(surface, ctx, start, end, list, region[1] - pixel);
85977 if (attr.transformFillStroke) {
85978 attr.matrix.toContext(ctx);
85979 }
85980 ctx.stroke();
85981 } else {
85982 // Prevent the reverse transform to fix floating point err.
85983 if (attr.fillArea) {
85984 ctx.fillStroke(attr, true);
85985 } else {
85986 ctx.stroke(true);
85987 }
85988 }
85989 }
85990 }
85991 });
85992
85993 /**
85994 * @class Ext.chart.series.Line
85995 * @extends Ext.chart.series.Cartesian
85996 *
85997 * Creates a Line Chart. A Line Chart is a useful visualization technique to display quantitative information for different
85998 * categories or other real values (as opposed to the bar chart), that can show some progression (or regression) in the dataset.
85999 * As with all other series, the Line Series must be appended in the *series* Chart array configuration. See the Chart
86000 * documentation for more information. A typical configuration object for the line series could be:
86001 *
86002 * @example preview
86003 * var lineChart = new Ext.chart.CartesianChart({
86004 * animate: true,
86005 * store: {
86006 * fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
86007 * data: [
86008 * {'name':'metric one', 'data1':10, 'data2':12, 'data3':14, 'data4':8, 'data5':13},
86009 * {'name':'metric two', 'data1':7, 'data2':8, 'data3':16, 'data4':10, 'data5':3},
86010 * {'name':'metric three', 'data1':5, 'data2':2, 'data3':14, 'data4':12, 'data5':7},
86011 * {'name':'metric four', 'data1':2, 'data2':14, 'data3':6, 'data4':1, 'data5':23},
86012 * {'name':'metric five', 'data1':27, 'data2':38, 'data3':36, 'data4':13, 'data5':33}
86013 * ]
86014 * },
86015 * axes: [{
86016 * type: 'numeric',
86017 * position: 'left',
86018 * fields: ['data1'],
86019 * title: {
86020 * text: 'Sample Values',
86021 * fontSize: 15
86022 * },
86023 * grid: true,
86024 * minimum: 0
86025 * }, {
86026 * type: 'category',
86027 * position: 'bottom',
86028 * fields: ['name'],
86029 * title: {
86030 * text: 'Sample Values',
86031 * fontSize: 15
86032 * }
86033 * }],
86034 * series: [{
86035 * type: 'line',
86036 * highlight: {
86037 * size: 7,
86038 * radius: 7
86039 * },
86040 * style: {
86041 * stroke: 'rgb(143,203,203)'
86042 * },
86043 * xField: 'name',
86044 * yField: 'data1',
86045 * marker: {
86046 * type: 'path',
86047 * path: ['M', -2, 0, 0, 2, 2, 0, 0, -2, 'Z'],
86048 * stroke: 'blue',
86049 * lineWidth: 0
86050 * }
86051 * }, {
86052 * type: 'line',
86053 * highlight: {
86054 * size: 7,
86055 * radius: 7
86056 * },
86057 * fill: true,
86058 * xField: 'name',
86059 * yField: 'data3',
86060 * marker: {
86061 * type: 'circle',
86062 * radius: 4,
86063 * lineWidth: 0
86064 * }
86065 * }]
86066 * });
86067 * Ext.Viewport.setLayout('fit');
86068 * Ext.Viewport.add(lineChart);
86069 *
86070 * In this configuration we're adding two series (or lines), one bound to the `data1`
86071 * property of the store and the other to `data3`. The type for both configurations is
86072 * `line`. The `xField` for both series is the same, the `name` property of the store.
86073 * Both line series share the same axis, the left axis. You can set particular marker
86074 * configuration by adding properties onto the markerConfig object. Both series have
86075 * an object as highlight so that markers animate smoothly to the properties in highlight
86076 * when hovered. The second series has `fill = true` which means that the line will also
86077 * have an area below it of the same color.
86078 *
86079 * **Note:** In the series definition remember to explicitly set the axis to bind the
86080 * values of the line series to. This can be done by using the `axis` configuration property.
86081 */
86082 Ext.define('Ext.chart.series.Line', {
86083 extend: Ext.chart.series.Cartesian ,
86084 alias: 'series.line',
86085 type: 'line',
86086 seriesType: 'lineSeries',
86087
86088
86089
86090
86091
86092 config: {
86093 /**
86094 * @cfg {Number} selectionTolerance
86095 * The offset distance from the cursor position to the line series to trigger events (then used for highlighting series, etc).
86096 */
86097 selectionTolerance: 20,
86098
86099 /**
86100 * @cfg {Object} style
86101 * An object containing styles for the visualization lines. These styles will override the theme styles.
86102 * Some options contained within the style object will are described next.
86103 */
86104
86105 /**
86106 * @cfg {Boolean/Number} smooth
86107 * If set to `true` or a non-zero number, the line will be smoothed/rounded around its points; otherwise
86108 * straight line segments will be drawn.
86109 *
86110 * A numeric value is interpreted as a divisor of the horizontal distance between consecutive points in
86111 * the line; larger numbers result in sharper curves while smaller numbers result in smoother curves.
86112 *
86113 * If set to `true` then a default numeric value of 3 will be used.
86114 */
86115 smooth: false,
86116
86117 /**
86118 * @cfg {Boolean} step
86119 * If set to `true`, the line uses steps instead of straight lines to connect the dots.
86120 * It is ignored if `smooth` is true.
86121 */
86122 step: false,
86123
86124 /**
86125 * @cfg {Boolean} fill
86126 * If set to `true`, the area underneath the line is filled with the color defined as follows, listed by priority:
86127 * - The color that is configured for this series ({@link Ext.chart.series.Series#colors}).
86128 * - The color that is configured for this chart ({@link Ext.chart.AbstractChart#colors}).
86129 * - The fill color that is set in the {@link #style} config.
86130 * - The stroke color that is set in the {@link #style} config, or the same color as the line.
86131 *
86132 * Note: Do not confuse `series.config.fill` (which is a boolean) with `series.style.fill' (which is an alias
86133 * for the `fillStyle` property and contains a color). For compatibility with previous versions of the API,
86134 * if `config.fill` is undefined but a `style.fill' color is provided, `config.fill` is considered true.
86135 * So the default value below must be undefined, not false.
86136 */
86137 fill: undefined,
86138
86139 aggregator: { strategy: 'double' }
86140 },
86141
86142 /**
86143 * @private Default numeric smoothing value to be used when `{@link #smooth} = true`.
86144 */
86145 defaultSmoothness: 3,
86146
86147 /**
86148 * @private Size of the buffer area on either side of the viewport to provide seamless zoom/pan
86149 * transforms. Expressed as a multiple of the viewport length, e.g. 1 will make the buffer on
86150 * each side equal to the length of the visible axis viewport.
86151 */
86152 overflowBuffer: 1,
86153
86154 /**
86155 * @private Override {@link Ext.chart.series.Series#getDefaultSpriteConfig}
86156 */
86157 getDefaultSpriteConfig: function () {
86158 var me = this,
86159 parentConfig = me.callSuper(arguments),
86160 style = me.getStyle(),
86161 fillArea = false;
86162
86163 if (typeof me.config.fill != 'undefined') {
86164 // If config.fill is present but there is no fillStyle, then use the
86165 // strokeStyle to fill (and paint the area the same color as the line).
86166 if (me.config.fill) {
86167 fillArea = true;
86168 if (typeof style.fillStyle == 'undefined') {
86169 style.fillStyle = style.strokeStyle;
86170 }
86171 }
86172 } else {
86173 // For compatibility with previous versions of the API, if config.fill
86174 // is undefined but style.fillStyle is provided, we fill the area.
86175 if (style.fillStyle) {
86176 fillArea = true;
86177 }
86178 }
86179
86180 // If we don't fill, then delete the fillStyle because that's what is used by
86181 // the Line sprite to fill below the line.
86182 if (!fillArea) {
86183 delete style.fillStyle;
86184 }
86185
86186 return Ext.apply(parentConfig || {}, {
86187 fillArea: fillArea,
86188 step: me.config.step,
86189 smooth: me.config.smooth,
86190 selectionTolerance: me.config.selectionTolerance
86191 });
86192 }
86193
86194 });
86195
86196 /**
86197 * Polar series.
86198 */
86199 Ext.define('Ext.chart.series.Polar', {
86200
86201 extend: Ext.chart.series.Series ,
86202
86203 config: {
86204 /**
86205 * @cfg {Number} rotation
86206 * The angle in degrees at which the first polar series item should start.
86207 */
86208 rotation: 0,
86209
86210 /**
86211 * @cfg {Number} radius
86212 * The radius of the polar series. Set to `null` will fit the polar series to the boundary.
86213 */
86214 radius: null,
86215
86216 /**
86217 * @cfg {Array} center for the polar series.
86218 */
86219 center: [0, 0],
86220
86221 /**
86222 * @cfg {Number} offsetX
86223 * The x-offset of center of the polar series related to the center of the boundary.
86224 */
86225 offsetX: 0,
86226
86227 /**
86228 * @cfg {Number} offsetY
86229 * The y-offset of center of the polar series related to the center of the boundary.
86230 */
86231 offsetY: 0,
86232
86233 /**
86234 * @cfg {Boolean} showInLegend
86235 * Whether to add the series elements as legend items.
86236 */
86237 showInLegend: true,
86238
86239 /**
86240 * @cfg {String} xField
86241 * The store record field name for the labels used in the radar series.
86242 */
86243 xField: null,
86244
86245 /**
86246 * @cfg {String} yField
86247 * The store record field name for the deflection of the graph in the radar series.
86248 */
86249 yField: null,
86250
86251 xAxis: null,
86252
86253 yAxis: null
86254 },
86255
86256 directions: ['X', 'Y'],
86257
86258 fieldCategoryX: ['X'],
86259 fieldCategoryY: ['Y'],
86260
86261 getDefaultSpriteConfig: function () {
86262 return {
86263 type: this.seriesType,
86264 renderer: this.getRenderer(),
86265 centerX: 0,
86266 centerY: 0,
86267 rotationCenterX: 0,
86268 rotationCenterY: 0
86269 };
86270 },
86271
86272 applyRotation: function (rotation) {
86273 var twoPie = Math.PI * 2;
86274 return (rotation % twoPie + Math.PI) % twoPie - Math.PI;
86275 },
86276
86277 updateRotation: function (rotation) {
86278 var sprites = this.getSprites();
86279 if (sprites && sprites[0]) {
86280 sprites[0].setAttributes({
86281 baseRotation: rotation
86282 });
86283 }
86284 }
86285 });
86286
86287 /**
86288 * @class Ext.chart.series.sprite.PieSlice
86289 *
86290 * Pie slice sprite.
86291 */
86292 Ext.define('Ext.chart.series.sprite.PieSlice', {
86293 alias: 'sprite.pieslice',
86294 mixins: {
86295 markerHolder: Ext.chart.MarkerHolder
86296 },
86297 extend: Ext.draw.sprite.Sector ,
86298
86299 inheritableStatics: {
86300 def: {
86301 processors: {
86302 /**
86303 * @cfg {Boolean} [doCallout=true] 'true' if the pie series uses label callouts.
86304 */
86305 doCallout: 'bool',
86306
86307 /**
86308 * @cfg {Boolean} [rotateLabels=true] 'true' if the labels are rotated for easier reading.
86309 */
86310 rotateLabels: 'bool',
86311
86312 /**
86313 * @cfg {String} [label=''] Label associated with the Pie sprite.
86314 */
86315 label: 'string',
86316
86317 /**
86318 * @cfg {Number} [labelOverflowPadding=10] Padding around labels to determine overlap.
86319 * Any negative number allows the labels to overlap.
86320 */
86321 labelOverflowPadding: 'number',
86322
86323 renderer: 'default'
86324 },
86325 defaults: {
86326 doCallout: true,
86327 rotateLabels: true,
86328 label: '',
86329 labelOverflowPadding: 10,
86330 renderer: null
86331 }
86332 }
86333 },
86334
86335 config: {
86336 /**
86337 * @private
86338 * @cfg {Object} rendererData The object that is passed to the renderer.
86339 *
86340 * For instance when the PieSlice sprite is used in a Gauge chart, the object
86341 * contains the 'store' and 'field' properties, and the 'value' as well
86342 * for that one PieSlice that is used to draw the needle of the Gauge.
86343 */
86344 rendererData: null,
86345 rendererIndex: 0
86346 },
86347
86348 render: function (ctx, surface, clipRegion) {
86349 var me = this,
86350 attr = me.attr,
86351 itemCfg = {},
86352 changes;
86353
86354 if (attr.renderer) {
86355 itemCfg = {
86356 type: 'sector',
86357 text: attr.text,
86358 centerX: attr.centerX,
86359 centerY: attr.centerY,
86360 margin: attr.margin,
86361 startAngle: Math.min(attr.startAngle, attr.endAngle),
86362 endAngle: Math.max(attr.startAngle, attr.endAngle),
86363 startRho: Math.min(attr.startRho, attr.endRho),
86364 endRho: Math.max(attr.startRho, attr.endRho)
86365 };
86366 changes = attr.renderer.call(me, me, itemCfg, me.rendererData, me.rendererIndex);
86367 Ext.apply(me.attr, changes);
86368 }
86369
86370 // Draw the sector
86371 me.callSuper(arguments);
86372
86373 // Draw the labels
86374 if (attr.label && me.getBoundMarker('labels')) {
86375 me.placeLabel();
86376 }
86377 },
86378
86379 placeLabel: function () {
86380 var me = this,
86381 attr = me.attr,
86382 startAngle = Math.min(attr.startAngle, attr.endAngle),
86383 endAngle = Math.max(attr.startAngle, attr.endAngle),
86384 midAngle = (startAngle + endAngle) * 0.5,
86385 margin = attr.margin,
86386 centerX = attr.centerX,
86387 centerY = attr.centerY,
86388 startRho = Math.min(attr.startRho, attr.endRho) + margin,
86389 endRho = Math.max(attr.startRho, attr.endRho) + margin,
86390 midRho = (startRho + endRho) * 0.5,
86391 surfaceMatrix = me.surfaceMatrix,
86392 labelCfg = me.labelCfg || (me.labelCfg = {}),
86393 labelTpl = me.getBoundMarker('labels')[0].getTemplate(),
86394 labelBox, x, y, changes;
86395
86396 surfaceMatrix.appendMatrix(attr.matrix);
86397
86398 labelCfg.text = attr.label;
86399
86400 x = centerX + Math.cos(midAngle) * midRho;
86401 y = centerY + Math.sin(midAngle) * midRho;
86402 labelCfg.x = surfaceMatrix.x(x, y);
86403 labelCfg.y = surfaceMatrix.y(x, y);
86404
86405 x = centerX + Math.cos(midAngle) * endRho;
86406 y = centerY + Math.sin(midAngle) * endRho;
86407 labelCfg.calloutStartX = surfaceMatrix.x(x, y);
86408 labelCfg.calloutStartY = surfaceMatrix.y(x, y);
86409
86410 x = centerX + Math.cos(midAngle) * (endRho + 40);
86411 y = centerY + Math.sin(midAngle) * (endRho + 40);
86412 labelCfg.calloutPlaceX = surfaceMatrix.x(x, y);
86413 labelCfg.calloutPlaceY = surfaceMatrix.y(x, y);
86414
86415 labelCfg.rotationRads = (attr.rotateLabels ? midAngle + Math.atan2(surfaceMatrix.y(1, 0) - surfaceMatrix.y(0, 0), surfaceMatrix.x(1, 0) - surfaceMatrix.x(0, 0)) : 0);
86416 labelCfg.calloutColor = me.attr.fillStyle;
86417 labelCfg.globalAlpha = attr.globalAlpha * attr.fillOpacity;
86418
86419 // If a slice is empty, don't display the label.
86420 // This behavior can be overridden by a renderer.
86421 labelCfg.hidden = (attr.startAngle == attr.endAngle);
86422
86423 if (attr.renderer) {
86424 labelCfg.type = 'label';
86425 changes = attr.renderer.call(me, me, labelCfg, me.rendererData, me.rendererIndex);
86426 Ext.apply(labelCfg, changes);
86427 }
86428 me.putMarker('labels', labelCfg, me.attr.attributeId);
86429
86430 labelBox = me.getMarkerBBox('labels', me.attr.attributeId, true);
86431 if (labelBox) {
86432 if (attr.doCallout) {
86433 if (labelTpl.attr.display === 'outside') {
86434 me.putMarker('labels', {callout: 1}, me.attr.attributeId);
86435 } else {
86436 me.putMarker('labels', {callout: 1 - +me.sliceContainsLabel(attr, labelBox)}, me.attr.attributeId);
86437 }
86438 } else {
86439 me.putMarker('labels', {globalAlpha: +me.sliceContainsLabel(attr, labelBox)}, me.attr.attributeId);
86440 }
86441 }
86442 },
86443
86444 sliceContainsLabel: function (attr, bbox) {
86445 var padding = attr.labelOverflowPadding,
86446 middle = (attr.endRho + attr.startRho) / 2,
86447 outer = middle + (bbox.width + padding) / 2,
86448 inner = middle - (bbox.width + padding) / 2,
86449 sliceAngle, l1, l2, l3;
86450
86451 if (padding < 0) {
86452 return 1;
86453 }
86454 if (bbox.width + padding * 2 > (attr.endRho - attr.startRho)) {
86455 return 0;
86456 }
86457 l1 = Math.sqrt(attr.endRho * attr.endRho - outer * outer);
86458 l2 = Math.sqrt(attr.endRho * attr.endRho - inner * inner);
86459 sliceAngle = Math.abs(attr.endAngle - attr.startAngle);
86460 l3 = (sliceAngle > Math.PI/2 ? inner : Math.abs(Math.tan(sliceAngle / 2)) * inner);
86461 if (bbox.height + padding * 2 > Math.min(l1, l2, l3) * 2) {
86462 return 0;
86463 }
86464 return 1;
86465 }
86466 });
86467
86468 /**
86469 * @class Ext.chart.series.Pie
86470 * @extends Ext.chart.series.Polar
86471 *
86472 * Creates a Pie Chart. A Pie Chart is a useful visualization technique to display quantitative information for different
86473 * categories that also have a meaning as a whole.
86474 * As with all other series, the Pie Series must be appended in the *series* Chart array configuration. See the Chart
86475 * documentation for more information. A typical configuration object for the pie series could be:
86476 *
86477 * @example preview
86478 * var chart = new Ext.chart.PolarChart({
86479 * animate: true,
86480 * interactions: ['rotate'],
86481 * colors: ['#115fa6', '#94ae0a', '#a61120', '#ff8809', '#ffd13e'],
86482 * store: {
86483 * fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
86484 * data: [
86485 * {name: 'metric one', data1: 10, data2: 12, data3: 14, data4: 8, data5: 13},
86486 * {name: 'metric two', data1: 7, data2: 8, data3: 16, data4: 10, data5: 3},
86487 * {name: 'metric three', data1: 5, data2: 2, data3: 14, data4: 12, data5: 7},
86488 * {name: 'metric four', data1: 2, data2: 14, data3: 6, data4: 1, data5: 23},
86489 * {name: 'metric five', data1: 27, data2: 38, data3: 36, data4: 13, data5: 33}
86490 * ]
86491 * },
86492 * series: [{
86493 * type: 'pie',
86494 * label: {
86495 * field: 'name',
86496 * display: 'rotate'
86497 * },
86498 * xField: 'data3',
86499 * donut: 30
86500 * }]
86501 * });
86502 * Ext.Viewport.setLayout('fit');
86503 * Ext.Viewport.add(chart);
86504 *
86505 * In this configuration we set `pie` as the type for the series, set an object with specific style properties for highlighting options
86506 * (triggered when hovering elements). We also set true to `showInLegend` so all the pie slices can be represented by a legend item.
86507 * We set `data1` as the value of the field to determine the angle span for each pie slice. We also set a label configuration object
86508 * where we set the field name of the store field to be rendered as text for the label. The labels will also be displayed rotated.
86509 * We set `contrast` to `true` to flip the color of the label if it is to similar to the background color. Finally, we set the font family
86510 * and size through the `font` parameter.
86511 *
86512 */
86513 Ext.define('Ext.chart.series.Pie', {
86514 extend: Ext.chart.series.Polar ,
86515
86516
86517
86518 type: 'pie',
86519 alias: 'series.pie',
86520 seriesType: 'pieslice',
86521
86522 config: {
86523 /**
86524 * @cfg {String} labelField
86525 * @deprecated Use {@link Ext.chart.series.Pie#label} instead.
86526 * The store record field name to be used for the pie slice labels.
86527 */
86528 labelField: false,
86529
86530 /**
86531 * @cfg {Number} donut Specifies the radius of the donut hole, as a percentage of the chart's radius.
86532 * Defaults to 0 (no donut hole).
86533 */
86534 donut: 0,
86535
86536 /**
86537 * @cfg {String} field
86538 * @deprecated Use xField directly
86539 */
86540 field: null,
86541
86542 /**
86543 * @cfg {Number} rotation The starting angle of the pie slices.
86544 */
86545 rotation: 0,
86546
86547 /**
86548 * @cfg {Number} [totalAngle=2*PI] The total angle of the pie series.
86549 */
86550 totalAngle: Math.PI * 2,
86551
86552 /**
86553 * @cfg {Array} hidden Determines which pie slices are hidden.
86554 */
86555 hidden: [],
86556
86557 /**
86558 * @cfg {Number} Allows adjustment of the radius by a spefic perfentage.
86559 */
86560 radiusFactor: 100,
86561
86562 style: {
86563
86564 }
86565 },
86566
86567 directions: ['X'],
86568
86569 setField: function (f) {
86570 return this.setXField(f);
86571 },
86572
86573 getField: function () {
86574 return this.getXField();
86575 },
86576
86577 applyRadius : function (radius) {
86578 return radius * this.getRadiusFactor() * 0.01;
86579 },
86580
86581 updateLabelData: function () {
86582 var me = this,
86583 store = me.getStore(),
86584 items = store.getData().items,
86585 sprites = me.getSprites(),
86586 labelField = me.getLabel().getTemplate().getField(),
86587 hidden = me.getHidden(),
86588 i, ln, labels, sprite;
86589 if (sprites.length > 0 && labelField) {
86590 labels = [];
86591 for (i = 0, ln = items.length; i < ln; i++) {
86592 labels.push(items[i].get(labelField));
86593 }
86594 for (i = 0, ln = sprites.length; i < ln; i++) {
86595 sprite = sprites[i];
86596 sprite.setAttributes({label: labels[i]});
86597 sprite.putMarker('labels', {hidden: hidden[i]}, sprite.attr.attributeId);
86598 }
86599 }
86600 },
86601
86602 coordinateX: function () {
86603 var me = this,
86604 store = me.getStore(),
86605 items = store.getData().items,
86606 length = items.length,
86607 field = me.getXField(),
86608 value, sum = 0,
86609 hidden = me.getHidden(),
86610 summation = [], i,
86611 lastAngle = 0,
86612 totalAngle = me.getTotalAngle(),
86613 sprites = me.getSprites();
86614
86615 if (!sprites) {
86616 return;
86617 }
86618
86619 for (i = 0; i < length; i++) {
86620 value = Math.abs(Number(items[i].get(field))) || 0;
86621 if (!hidden[i]) {
86622 sum += value;
86623 }
86624 summation[i] = sum;
86625 if (i >= hidden.length) {
86626 hidden[i] = false;
86627 }
86628 }
86629
86630 if (sum !== 0) {
86631 sum = totalAngle / sum;
86632 }
86633 for (i = 0; i < length; i++) {
86634 sprites[i].setAttributes({
86635 startAngle: lastAngle,
86636 endAngle: lastAngle = (sum ? summation[i] * sum : 0),
86637 globalAlpha: 1
86638 });
86639 }
86640 for (; i < me.sprites.length; i++) {
86641 sprites[i].setAttributes({
86642 startAngle: totalAngle,
86643 endAngle: totalAngle,
86644 globalAlpha: 0
86645 });
86646 }
86647 me.getChart().refreshLegendStore();
86648 },
86649
86650 updateCenter: function (center) {
86651 this.setStyle({
86652 translationX: center[0] + this.getOffsetX(),
86653 translationY: center[1] + this.getOffsetY()
86654 });
86655 this.doUpdateStyles();
86656 },
86657
86658 updateRadius: function (radius) {
86659 this.setStyle({
86660 startRho: radius * this.getDonut() * 0.01, // Percentage
86661 endRho: radius
86662 });
86663 this.doUpdateStyles();
86664 },
86665
86666 updateDonut: function (donut) {
86667 var radius = this.getRadius();
86668 this.setStyle({
86669 startRho: radius * donut * 0.01, // Percentage
86670 endRho: radius
86671 });
86672 this.doUpdateStyles();
86673 },
86674
86675 updateRotation: function (rotation) {
86676 this.setStyle({
86677 rotationRads: rotation
86678 });
86679 this.doUpdateStyles();
86680 },
86681
86682 updateTotalAngle: function (totalAngle) {
86683 this.processData();
86684 },
86685
86686 getSprites: function () {
86687 var me = this,
86688 chart = me.getChart(),
86689 store = me.getStore();
86690 if (!chart || !store) {
86691 return [];
86692 }
86693 me.getColors();
86694 me.getSubStyle();
86695 var items = store.getData().items,
86696 length = items.length,
86697 animation = chart && chart.getAnimate(),
86698 sprites = me.sprites, sprite,
86699 spriteIndex = 0, rendererData,
86700 i, spriteCreated = false,
86701 label = me.getLabel(),
86702 labelTpl = label.getTemplate();
86703
86704 rendererData = {
86705 store: store,
86706 field: me.getField(),
86707 series: me
86708 };
86709
86710 for (i = 0; i < length; i++) {
86711 sprite = sprites[i];
86712 if (!sprite) {
86713 sprite = me.createSprite();
86714 if (me.getHighlightCfg()) {
86715 sprite.config.highlightCfg = me.getHighlightCfg();
86716 sprite.addModifier('highlight', true);
86717 }
86718 if (labelTpl.getField()) {
86719 labelTpl.setAttributes({
86720 labelOverflowPadding: me.getLabelOverflowPadding()
86721 });
86722 labelTpl.fx.setCustomDuration({'callout': 200});
86723 sprite.bindMarker('labels', label);
86724 }
86725 sprite.setAttributes(me.getStyleByIndex(i));
86726 sprite.rendererData = rendererData;
86727 sprite.rendererIndex = spriteIndex++;
86728 spriteCreated = true;
86729 }
86730 sprite.fx.setConfig(animation);
86731 }
86732 if (spriteCreated) {
86733 me.doUpdateStyles();
86734 }
86735 return me.sprites;
86736 },
86737
86738 normalizeAngle: function (angle) {
86739 var pi2 = Math.PI * 2;
86740 if (angle >= 0) {
86741 return angle % pi2;
86742 }
86743 return (angle % pi2 + pi2) % pi2;
86744 },
86745
86746 betweenAngle: function (x, a, b) {
86747 var normalize = this.normalizeAngle;
86748 a = normalize(a);
86749 b = normalize(b);
86750 x = normalize(x);
86751 if (b === 0) {
86752 b = Math.PI * 2;
86753 }
86754 return x >= a && x < b;
86755 },
86756
86757 /**
86758 * Returns the pie slice for a given angle
86759 * @param {Number} angle The angle to search for the slice
86760 * @return {Object} An object containing the reocord, sprite, scope etc.
86761 */
86762 getItemForAngle: function (angle) {
86763 var me = this,
86764 sprites = me.getSprites(),
86765 attr;
86766
86767 angle %= Math.PI * 2;
86768
86769 while (angle < 0) {
86770 angle += Math.PI * 2;
86771 }
86772
86773 if (sprites) {
86774 var store = me.getStore(),
86775 items = store.getData().items,
86776 hidden = me.getHidden(),
86777 i = 0,
86778 ln = store.getCount();
86779
86780 for (; i < ln; i++) {
86781 if(!hidden[i]) {
86782 // Fortunately, the id of items equals the index of it in instances list.
86783 attr = sprites[i].attr;
86784
86785 if (attr.startAngle <= angle && attr.endAngle >= angle) {
86786 return {
86787 series: me,
86788 sprite: sprites[i],
86789 index: i,
86790 record: items[i],
86791 field: me.getXField()
86792 };
86793 }
86794 }
86795 }
86796 }
86797
86798 return null;
86799 },
86800
86801 getItemForPoint: function (x, y) {
86802 var me = this,
86803 sprites = me.getSprites();
86804 if (sprites) {
86805 var center = me.getCenter(),
86806 offsetX = me.getOffsetX(),
86807 offsetY = me.getOffsetY(),
86808 originalX = x - center[0] + offsetX,
86809 originalY = y - center[1] + offsetY,
86810 store = me.getStore(),
86811 donut = me.getDonut(),
86812 items = store.getData().items,
86813 direction = Math.atan2(originalY, originalX) - me.getRotation(),
86814 donutLimit = Math.sqrt(originalX * originalX + originalY * originalY),
86815 endRadius = me.getRadius(),
86816 startRadius = donut / 100 * endRadius,
86817 hidden = me.getHidden(),
86818 i, ln, attr;
86819
86820 for (i = 0, ln = items.length; i < ln; i++) {
86821 if(!hidden[i]) {
86822 // Fortunately, the id of items equals the index of it in instances list.
86823 attr = sprites[i].attr;
86824 if (startRadius + attr.margin <= donutLimit && donutLimit + attr.margin <= endRadius) {
86825 if (this.betweenAngle(direction, attr.startAngle, attr.endAngle)) {
86826 return {
86827 series: this,
86828 sprite: sprites[i],
86829 index: i,
86830 record: items[i],
86831 field: this.getXField()
86832 };
86833 }
86834 }
86835 }
86836 }
86837 return null;
86838 }
86839 },
86840
86841 provideLegendInfo: function (target) {
86842 var store = this.getStore();
86843 if (store) {
86844 var items = store.getData().items,
86845 labelField = this.getLabel().getTemplate().getField(),
86846 field = this.getField(),
86847 hidden = this.getHidden();
86848 for (var i = 0; i < items.length; i++) {
86849 target.push({
86850 name: labelField ? String(items[i].get(labelField)) : field + ' ' + i,
86851 mark: this.getStyleByIndex(i).fillStyle || this.getStyleByIndex(i).strokeStyle || 'black',
86852 disabled: hidden[i],
86853 series: this.getId(),
86854 index: i
86855 });
86856 }
86857 }
86858 }
86859 });
86860
86861
86862 /**
86863 * @class Ext.chart.series.sprite.Pie3DPart
86864 * @extends Ext.draw.sprite.Path
86865 *
86866 * Pie3D series sprite.
86867 */
86868 Ext.define("Ext.chart.series.sprite.Pie3DPart", {
86869 extend: Ext.draw.sprite.Path ,
86870 mixins: {
86871 markerHolder: Ext.chart.MarkerHolder
86872 },
86873 alias: 'sprite.pie3dPart',
86874 type: 'pie3dPart',
86875 inheritableStatics: {
86876 def: {
86877 processors: {
86878 /**
86879 * @cfg {Number} [centerX=0] The central point of the series on the x-axis.
86880 */
86881 centerX: "number",
86882
86883 /**
86884 * @cfg {Number} [centerY=0] The central point of the series on the x-axis.
86885 */
86886 centerY: "number",
86887
86888 /**
86889 * @cfg {Number} [startAngle=0] The starting angle of the polar series.
86890 */
86891 startAngle: "number",
86892
86893 /**
86894 * @cfg {Number} [endAngle=Math.PI] The ending angle of the polar series.
86895 */
86896 endAngle: "number",
86897
86898 /**
86899 * @cfg {Number} [startRho=0] The starting radius of the polar series.
86900 */
86901 startRho: "number",
86902
86903 /**
86904 * @cfg {Number} [endRho=150] The ending radius of the polar series.
86905 */
86906 endRho: "number",
86907
86908 /**
86909 * @cfg {Number} [margin=0] Margin from the center of the pie. Used for donut.
86910 */
86911 margin: "number",
86912
86913 /**
86914 * @cfg {Number} [thickness=0] The thickness of the 3D pie part.
86915 */
86916 thickness: "number",
86917
86918 /**
86919 * @cfg {Number} [distortion=0] The distortion of the 3D pie part.
86920 */
86921 distortion: "number",
86922
86923 /**
86924 * @cfg {Object} [baseColor='white'] The color of the 3D pie part before adding the 3D effect.
86925 */
86926 baseColor: "color",
86927
86928 /**
86929 * @cfg {Number} [baseRotation=0] The starting rotation of the polar series.
86930 */
86931 baseRotation: "number",
86932
86933 /**
86934 * @cfg {String} [part=0] The part of the 3D Pie represented by the sprite.
86935 */
86936 part: "enums(top,start,end,inner,outer)"
86937 },
86938 aliases: {
86939 rho: 'endRho'
86940 },
86941 dirtyTriggers: {
86942 centerX: "path,bbox",
86943 centerY: "path,bbox",
86944 startAngle: "path,partZIndex",
86945 endAngle: "path,partZIndex",
86946 startRho: "path",
86947 endRho: "path,bbox",
86948 margin: "path,bbox",
86949 thickness: "path",
86950 baseRotation: "path,partZIndex,partColor",
86951 baseColor: 'partZIndex,partColor',
86952 part: "path,partZIndex"
86953 },
86954 defaults: {
86955 centerX: 0,
86956 centerY: 0,
86957 startAngle: 0,
86958 endAngle: 0,
86959 startRho: 0,
86960 endRho: 150,
86961 margin: 0,
86962 distortion: 1,
86963 baseRotation: 0,
86964 baseColor: 'white',
86965 part: "top"
86966 },
86967 updaters: {
86968 "partColor": function (attrs) {
86969 var color = Ext.draw.Color.fly(attrs.baseColor),
86970 fillStyle;
86971 switch (attrs.part) {
86972 case 'top':
86973 fillStyle = color.toString();
86974 break;
86975 case 'outer':
86976 fillStyle = Ext.create("Ext.draw.gradient.Linear", {
86977 type: 'linear',
86978 stops: [
86979 {
86980 offset: 0,
86981 color: color.createDarker(0.3).toString()
86982 },
86983 {
86984 offset: 0.3,
86985 color: color.toString()
86986 },
86987 {
86988 offset: 0.8,
86989 color: color.createLighter(0.2).toString()
86990 },
86991 {
86992 offset: 1,
86993 color: color.createDarker(0.4).toString()
86994 }
86995 ]
86996 });
86997 break;
86998 case 'start':
86999 fillStyle = color.createDarker(0.3).toString();
87000 break;
87001 case 'end':
87002 fillStyle = color.createDarker(0.3).toString();
87003 break;
87004 case 'inner':
87005 fillStyle = Ext.create("Ext.draw.gradient.Linear", {
87006 type: 'linear',
87007 stops: [
87008 {
87009 offset: 0,
87010 color: color.createDarker(0.4).toString()
87011 },
87012 {
87013 offset: 0.2,
87014 color: color.createLighter(0.2).toString()
87015 },
87016 {
87017 offset: 0.7,
87018 color: color.toString()
87019 },
87020 {
87021 offset: 1,
87022 color: color.createDarker(0.3).toString()
87023 }
87024 ]
87025 });
87026 break;
87027 }
87028
87029 attrs.fillStyle = fillStyle;
87030 attrs.canvasAttributes.fillStyle = fillStyle;
87031 },
87032 "partZIndex": function (attrs) {
87033 var rotation = attrs.baseRotation;
87034 switch (attrs.part) {
87035 case 'top':
87036 attrs.zIndex = 5;
87037 break;
87038 case 'outer':
87039 attrs.zIndex = 4;
87040 break;
87041 case 'start':
87042 attrs.zIndex = 1 + Math.sin(attrs.startAngle + rotation);
87043 break;
87044 case 'end':
87045 attrs.zIndex = 1 + Math.sin(attrs.endAngle + rotation);
87046 break;
87047 case 'inner':
87048 attrs.zIndex = 1;
87049 break;
87050 }
87051 attrs.dirtyZIndex = true;
87052 }
87053 }
87054 }
87055 },
87056
87057 updatePlainBBox: function (plain) {
87058 var attr = this.attr,
87059 rho = attr.part === 'inner' ? attr.startRho : attr.endRho;
87060 plain.width = rho * 2;
87061 plain.height = rho * attr.distortion * 2 + attr.thickness;
87062 plain.x = attr.centerX - rho;
87063 plain.y = attr.centerY - rho * attr.distortion;
87064 },
87065
87066 updateTransformedBBox: function (transform) {
87067 return this.updatePlainBBox(transform);
87068 },
87069
87070 updatePath: function (path) {
87071 if (this.attr.endAngle < this.attr.startAngle) {
87072 return;
87073 }
87074 this[this.attr.part + 'Renderer'](path);
87075 },
87076
87077 topRenderer: function (path) {
87078 var attr = this.attr,
87079 margin = attr.margin,
87080 distortion = attr.distortion,
87081 centerX = attr.centerX,
87082 centerY = attr.centerY,
87083 baseRotation = attr.baseRotation,
87084 startAngle = attr.startAngle + baseRotation ,
87085 endAngle = attr.endAngle + baseRotation ,
87086 startRho = attr.startRho,
87087 endRho = attr.endRho,
87088 midAngle,
87089 sinEnd = Math.sin(endAngle),
87090 cosEnd = Math.cos(endAngle);
87091 midAngle = (startAngle + endAngle) * 0.5;
87092 centerX += Math.cos(midAngle) * margin;
87093 centerY += Math.sin(midAngle) * margin * distortion;
87094 path.ellipse(centerX, centerY, startRho, startRho * distortion, 0, startAngle, endAngle, false);
87095 path.lineTo(centerX + cosEnd * endRho, centerY + sinEnd * endRho * distortion);
87096 path.ellipse(centerX, centerY, endRho, endRho * distortion, 0, endAngle, startAngle, true);
87097 path.closePath();
87098 },
87099
87100 startRenderer: function (path) {
87101 var attr = this.attr,
87102 margin = attr.margin,
87103 centerX = attr.centerX,
87104 centerY = attr.centerY,
87105 distortion = attr.distortion,
87106 baseRotation = attr.baseRotation,
87107 startAngle = attr.startAngle + baseRotation ,
87108 endAngle = attr.endAngle + baseRotation,
87109 thickness = attr.thickness,
87110 startRho = attr.startRho,
87111 endRho = attr.endRho,
87112 sinStart = Math.sin(startAngle),
87113 cosStart = Math.cos(startAngle),
87114 midAngle;
87115 if (cosStart < 0) {
87116 midAngle = (startAngle + endAngle) * 0.5;
87117 centerX += Math.cos(midAngle) * margin;
87118 centerY += Math.sin(midAngle) * margin * distortion;
87119 path.moveTo(centerX + cosStart * startRho, centerY + sinStart * startRho * distortion);
87120 path.lineTo(centerX + cosStart * endRho, centerY + sinStart * endRho * distortion);
87121 path.lineTo(centerX + cosStart * endRho, centerY + sinStart * endRho * distortion + thickness);
87122 path.lineTo(centerX + cosStart * startRho, centerY + sinStart * startRho * distortion + thickness);
87123 path.closePath();
87124 }
87125 },
87126
87127 endRenderer: function (path) {
87128 var attr = this.attr,
87129 margin = attr.margin,
87130 centerX = attr.centerX,
87131 centerY = attr.centerY,
87132 distortion = attr.distortion,
87133 baseRotation = attr.baseRotation,
87134 startAngle = attr.startAngle + baseRotation ,
87135 endAngle = attr.endAngle + baseRotation,
87136 thickness = attr.thickness,
87137 startRho = attr.startRho,
87138 endRho = attr.endRho,
87139 sin = Math.sin(endAngle),
87140 cos = Math.cos(endAngle), midAngle;
87141 if (cos > 0) {
87142 midAngle = (startAngle + endAngle) * 0.5;
87143 centerX += Math.cos(midAngle) * margin;
87144 centerY += Math.sin(midAngle) * margin * distortion;
87145 path.moveTo(centerX + cos * startRho, centerY + sin * startRho * distortion);
87146 path.lineTo(centerX + cos * endRho, centerY + sin * endRho * distortion);
87147 path.lineTo(centerX + cos * endRho, centerY + sin * endRho * distortion + thickness);
87148 path.lineTo(centerX + cos * startRho, centerY + sin * startRho * distortion + thickness);
87149 path.closePath();
87150 }
87151 },
87152
87153 innerRenderer: function (path) {
87154 var attr = this.attr,
87155 margin = attr.margin,
87156 centerX = attr.centerX,
87157 centerY = attr.centerY,
87158 distortion = attr.distortion,
87159 baseRotation = attr.baseRotation,
87160 startAngle = attr.startAngle + baseRotation ,
87161 endAngle = attr.endAngle + baseRotation,
87162 thickness = attr.thickness,
87163 startRho = attr.startRho,
87164 sinEnd, cosEnd,
87165 tempStart, tempEnd, midAngle;
87166 midAngle = (startAngle + endAngle) * 0.5;
87167 centerX += Math.cos(midAngle) * margin;
87168 centerY += Math.sin(midAngle) * margin * distortion;
87169 if (startAngle >= Math.PI * 2) {
87170 startAngle -= Math.PI * 2;
87171 endAngle -= Math.PI * 2;
87172 }
87173 if (endAngle > Math.PI && endAngle < Math.PI * 3) {
87174 tempStart = startAngle;
87175 tempEnd = Math.min(endAngle, Math.PI * 2);
87176 sinEnd = Math.sin(tempEnd);
87177 cosEnd = Math.cos(tempEnd);
87178 path.ellipse(centerX, centerY, startRho, startRho * distortion, 0, tempStart, tempEnd, false);
87179 path.lineTo(centerX + cosEnd * startRho, centerY + sinEnd * startRho * distortion + thickness);
87180 path.ellipse(centerX, centerY + thickness, startRho, startRho * distortion, 0, tempEnd, tempStart, true);
87181 path.closePath();
87182 }
87183 if (endAngle > Math.PI * 3) {
87184 tempStart = Math.PI;
87185 tempEnd = endAngle;
87186 sinEnd = Math.sin(tempEnd);
87187 cosEnd = Math.cos(tempEnd);
87188 path.ellipse(centerX, centerY, startRho, startRho * distortion, 0, tempStart, tempEnd, false);
87189 path.lineTo(centerX + cosEnd * startRho, centerY + sinEnd * startRho * distortion + thickness);
87190 path.ellipse(centerX, centerY + thickness, startRho, startRho * distortion, 0, tempEnd, tempStart, true);
87191 path.closePath();
87192 }
87193 },
87194
87195 outerRenderer: function (path) {
87196 var attr = this.attr,
87197 margin = attr.margin,
87198 centerX = attr.centerX,
87199 centerY = attr.centerY,
87200 distortion = attr.distortion,
87201 baseRotation = attr.baseRotation,
87202 startAngle = attr.startAngle + baseRotation ,
87203 endAngle = attr.endAngle + baseRotation,
87204 thickness = attr.thickness,
87205 endRho = attr.endRho,
87206 sinEnd, cosEnd,
87207 tempStart, tempEnd, midAngle;
87208 midAngle = (startAngle + endAngle) * 0.5;
87209 centerX += Math.cos(midAngle) * margin;
87210 centerY += Math.sin(midAngle) * margin * distortion;
87211
87212 if (startAngle >= Math.PI * 2) {
87213 startAngle -= Math.PI * 2;
87214 endAngle -= Math.PI * 2;
87215 }
87216
87217 if (startAngle < Math.PI) {
87218 tempStart = startAngle;
87219 tempEnd = Math.min(endAngle, Math.PI);
87220 sinEnd = Math.sin(tempEnd);
87221 cosEnd = Math.cos(tempEnd);
87222 path.ellipse(centerX, centerY, endRho, endRho * distortion, 0, tempStart, tempEnd, false);
87223 path.lineTo(centerX + cosEnd * endRho, centerY + sinEnd * endRho * distortion + thickness);
87224 path.ellipse(centerX, centerY + thickness, endRho, endRho * distortion, 0, tempEnd, tempStart, true);
87225 path.closePath();
87226 }
87227 if (endAngle > Math.PI * 2) {
87228 tempStart = Math.max(startAngle, Math.PI * 2);
87229 tempEnd = endAngle;
87230 sinEnd = Math.sin(tempEnd);
87231 cosEnd = Math.cos(tempEnd);
87232 path.ellipse(centerX, centerY, endRho, endRho * distortion, 0, tempStart, tempEnd, false);
87233 path.lineTo(centerX + cosEnd * endRho, centerY + sinEnd * endRho * distortion + thickness);
87234 path.ellipse(centerX, centerY + thickness, endRho, endRho * distortion, 0, tempEnd, tempStart, true);
87235 path.closePath();
87236 }
87237 }
87238 });
87239
87240 /**
87241 * @class Ext.chart.series.Pie3D
87242 * @extends Ext.chart.series.Polar
87243 *
87244 * Creates a 3D Pie Chart.
87245 *
87246 * @example preview
87247 * var chart = new Ext.chart.PolarChart({
87248 * animate: true,
87249 * interactions: ['rotate'],
87250 * colors: ["#115fa6", "#94ae0a", "#a61120", "#ff8809", "#ffd13e"],
87251 * store: {
87252 * fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
87253 * data: [
87254 * {'name':'metric one', 'data1':10, 'data2':12, 'data3':14, 'data4':8, 'data5':13},
87255 * {'name':'metric two', 'data1':7, 'data2':8, 'data3':16, 'data4':10, 'data5':3},
87256 * {'name':'metric three', 'data1':5, 'data2':2, 'data3':14, 'data4':12, 'data5':7},
87257 * {'name':'metric four', 'data1':2, 'data2':14, 'data3':6, 'data4':1, 'data5':23},
87258 * {'name':'metric five', 'data1':27, 'data2':38, 'data3':36, 'data4':13, 'data5':33}
87259 * ]
87260 * },
87261 * series: [{
87262 * type: 'pie3d',
87263 * field: 'data3',
87264 * donut: 30
87265 * }]
87266 * });
87267 * Ext.Viewport.setLayout('fit');
87268 * Ext.Viewport.add(chart);
87269 */
87270 Ext.define('Ext.chart.series.Pie3D', {
87271
87272 extend: Ext.chart.series.Polar ,
87273 type: 'pie3d',
87274 seriesType: 'pie3d',
87275 alias: 'series.pie3d',
87276 config: {
87277 region: [0, 0, 0, 0],
87278 thickness: 35,
87279 distortion: 0.5,
87280
87281 /**
87282 * @cfg {String} field (required)
87283 * The store record field name to be used for the pie angles.
87284 * The values bound to this field name must be positive real numbers.
87285 */
87286 field: false,
87287
87288 /**
87289 * @private
87290 * @cfg {String} lengthField
87291 * Not supported.
87292 */
87293 lengthField: false,
87294
87295 /**
87296 * @cfg {Boolean/Number} donut
87297 * Whether to set the pie chart as donut chart.
87298 * Can be set to a particular percentage to set the radius
87299 * of the donut chart.
87300 */
87301 donut: false,
87302
87303 rotation: 0
87304 },
87305
87306 applyRotation: function (rotation) {
87307 var twoPie = Math.PI * 2;
87308 return (rotation % twoPie + twoPie) % twoPie;
87309 },
87310
87311 updateRotation: function (rotation) {
87312 var sprites = this.getSprites(),
87313 i, ln;
87314 for (i = 0, ln = sprites.length; i < ln; i++) {
87315 sprites[i].setAttributes({
87316 baseRotation: rotation
87317 });
87318 }
87319 },
87320
87321 updateColors: function (colorSet) {
87322 this.setSubStyle({baseColor: colorSet});
87323 },
87324
87325 doUpdateStyles: function () {
87326 var sprites = this.getSprites(),
87327 i = 0, j = 0, ln = sprites && sprites.length;
87328 for (; i < ln; i += 5, j++) {
87329 sprites[i].setAttributes(this.getStyleByIndex(j));
87330 sprites[i + 1].setAttributes(this.getStyleByIndex(j));
87331 sprites[i + 2].setAttributes(this.getStyleByIndex(j));
87332 sprites[i + 3].setAttributes(this.getStyleByIndex(j));
87333 sprites[i + 4].setAttributes(this.getStyleByIndex(j));
87334 }
87335 },
87336
87337 processData: function () {
87338 var me = this,
87339 chart = me.getChart(),
87340 animation = chart && chart.getAnimate(),
87341 store = me.getStore(),
87342 items = store.getData().items,
87343 length = items.length,
87344 field = me.getField(),
87345 value, sum = 0, ratio,
87346 summation = [],
87347 i,
87348 sprites = this.getSprites(),
87349 lastAngle;
87350
87351 for (i = 0; i < length; i++) {
87352 value = items[i].get(field);
87353 sum += value;
87354 summation[i] = sum;
87355 }
87356 if (sum === 0) {
87357 return;
87358 }
87359 ratio = 2 * Math.PI / sum;
87360 for (i = 0; i < length; i++) {
87361 summation[i] *= ratio;
87362 }
87363
87364 for (i = 0; i < sprites.length; i++) {
87365 sprites[i].fx.setConfig(animation);
87366 }
87367
87368 for (i = 0, lastAngle = 0; i < length; i++) {
87369 var commonAttributes = {opacity: 1, startAngle: lastAngle, endAngle: summation[i]};
87370 sprites[i * 5].setAttributes(commonAttributes);
87371 sprites[i * 5 + 1].setAttributes(commonAttributes);
87372 sprites[i * 5 + 2].setAttributes(commonAttributes);
87373 sprites[i * 5 + 3].setAttributes(commonAttributes);
87374 sprites[i * 5 + 4].setAttributes(commonAttributes);
87375 lastAngle = summation[i];
87376 }
87377 },
87378
87379 getSprites: function () {
87380 var me = this,
87381 chart = this.getChart(),
87382 surface = me.getSurface(),
87383 store = me.getStore();
87384 if (!store) {
87385 return [];
87386 }
87387 var items = store.getData().items,
87388 length = items.length,
87389 animation = chart && chart.getAnimate(),
87390 region = chart.getMainRegion() || [0, 0, 1, 1],
87391 rotation = me.getRotation(),
87392 center = me.getCenter(),
87393 offsetX = me.getOffsetX(),
87394 offsetY = me.getOffsetY(),
87395 radius = Math.min((region[3] - me.getThickness() * 2) / me.getDistortion(), region[2]) / 2,
87396 commonAttributes = {
87397 centerX: center[0] + offsetX,
87398 centerY: center[1] + offsetY - me.getThickness() / 2,
87399 endRho: radius,
87400 startRho: radius * me.getDonut() / 100,
87401 thickness: me.getThickness(),
87402 distortion: me.getDistortion()
87403 }, sliceAttributes, twoPie = Math.PI * 2,
87404 topSprite, startSprite, endSprite, innerSideSprite, outerSideSprite,
87405 i;
87406
87407 for (i = 0; i < length; i++) {
87408 sliceAttributes = Ext.apply({}, this.getStyleByIndex(i), commonAttributes);
87409 topSprite = me.sprites[i * 5];
87410 if (!topSprite) {
87411 topSprite = surface.add({
87412 type: 'pie3dPart',
87413 part: 'top',
87414 startAngle: twoPie,
87415 endAngle: twoPie
87416 });
87417 startSprite = surface.add({
87418 type: 'pie3dPart',
87419 part: 'start',
87420 startAngle: twoPie,
87421 endAngle: twoPie
87422 });
87423 endSprite = surface.add({
87424 type: 'pie3dPart',
87425 part: 'end',
87426 startAngle: twoPie,
87427 endAngle: twoPie
87428 });
87429 innerSideSprite = surface.add({
87430 type: 'pie3dPart',
87431 part: 'inner',
87432 startAngle: twoPie,
87433 endAngle: twoPie,
87434 thickness: 0
87435 });
87436 outerSideSprite = surface.add({
87437 type: 'pie3dPart',
87438 part: 'outer',
87439 startAngle: twoPie,
87440 endAngle: twoPie,
87441 thickness: 0
87442 });
87443 topSprite.fx.setDurationOn('baseRotation', 0);
87444 startSprite.fx.setDurationOn('baseRotation', 0);
87445 endSprite.fx.setDurationOn('baseRotation', 0);
87446 innerSideSprite.fx.setDurationOn('baseRotation', 0);
87447 outerSideSprite.fx.setDurationOn('baseRotation', 0);
87448 topSprite.setAttributes(sliceAttributes);
87449 startSprite.setAttributes(sliceAttributes);
87450 endSprite.setAttributes(sliceAttributes);
87451 innerSideSprite.setAttributes(sliceAttributes);
87452 outerSideSprite.setAttributes(sliceAttributes);
87453 me.sprites.push(topSprite, startSprite, endSprite, innerSideSprite, outerSideSprite);
87454 } else {
87455 startSprite = me.sprites[i * 5 + 1];
87456 endSprite = me.sprites[i * 5 + 2];
87457 innerSideSprite = me.sprites[i * 5 + 3];
87458 outerSideSprite = me.sprites[i * 5 + 4];
87459 if (animation) {
87460 topSprite.fx.setConfig(animation);
87461 startSprite.fx.setConfig(animation);
87462 endSprite.fx.setConfig(animation);
87463 innerSideSprite.fx.setConfig(animation);
87464 outerSideSprite.fx.setConfig(animation);
87465 }
87466 topSprite.setAttributes(sliceAttributes);
87467 startSprite.setAttributes(sliceAttributes);
87468 endSprite.setAttributes(sliceAttributes);
87469 innerSideSprite.setAttributes(sliceAttributes);
87470 outerSideSprite.setAttributes(sliceAttributes);
87471 }
87472 }
87473
87474 for (i *= 5; i < me.sprites.length; i++) {
87475 me.sprites[i].fx.setConfig(animation);
87476 me.sprites[i].setAttributes({
87477 opacity: 0,
87478 startAngle: twoPie,
87479 endAngle: twoPie,
87480 baseRotation: rotation
87481 });
87482 }
87483
87484 return me.sprites;
87485 }
87486 });
87487
87488 /**
87489 * @class Ext.chart.series.sprite.Polar
87490 * @extends Ext.draw.sprite.Sprite
87491 *
87492 * Polar sprite.
87493 */
87494 Ext.define('Ext.chart.series.sprite.Polar', {
87495 mixins: {
87496 markerHolder: Ext.chart.MarkerHolder
87497 },
87498 extend: Ext.draw.sprite.Sprite ,
87499 inheritableStatics: {
87500 def: {
87501 processors: {
87502 /**
87503 * @cfg {Number} [dataMinX=0] Data minimum on the x-axis.
87504 */
87505 dataMinX: 'number',
87506
87507 /**
87508 * @cfg {Number} [dataMaxX=1] Data maximum on the x-axis.
87509 */
87510 dataMaxX: 'number',
87511
87512 /**
87513 * @cfg {Number} [dataMinY=0] Data minimum on the y-axis.
87514 */
87515 dataMinY: 'number',
87516
87517 /**
87518 * @cfg {Number} [dataMaxY=2] Data maximum on the y-axis.
87519 */
87520 dataMaxY: 'number',
87521
87522 /**
87523 * @cfg {Array} Data range derived from all the series bound to the x-axis.
87524 */
87525 rangeX: 'data',
87526 /**
87527 * @cfg {Array} Data range derived from all the series bound to the y-axis.
87528 */
87529 rangeY: 'data',
87530
87531 /**
87532 * @cfg {Object} [dataY=null] Data items on the y-axis.
87533 */
87534 dataY: 'data',
87535
87536 /**
87537 * @cfg {Object} [dataX=null] Data items on the x-axis.
87538 */
87539 dataX: 'data',
87540
87541 /**
87542 * @cfg {Number} [centerX=0] The central point of the series on the x-axis.
87543 */
87544 centerX: 'number',
87545
87546 /**
87547 * @cfg {Number} [centerY=0] The central point of the series on the y-axis.
87548 */
87549 centerY: 'number',
87550
87551 /**
87552 * @cfg {Number} [startAngle=0] The starting angle of the polar series.
87553 */
87554 startAngle: "number",
87555
87556 /**
87557 * @cfg {Number} [endAngle=Math.PI] The ending angle of the polar series.
87558 */
87559 endAngle: "number",
87560
87561 /**
87562 * @cfg {Number} [startRho=0] The starting radius of the polar series.
87563 */
87564 startRho: "number",
87565
87566 /**
87567 * @cfg {Number} [endRho=150] The ending radius of the polar series.
87568 */
87569 endRho: "number",
87570
87571 /**
87572 * @cfg {Number} [baseRotation=0] The starting rotation of the polar series.
87573 */
87574 baseRotation: "number",
87575
87576 /**
87577 * @cfg {Object} [labels=null] Labels used in the series.
87578 */
87579 labels: 'default',
87580
87581 /**
87582 * @cfg {Number} [labelOverflowPadding=10] Padding around labels to determine overlap.
87583 */
87584 labelOverflowPadding: 'number'
87585 },
87586 defaults: {
87587 dataY: null,
87588 dataX: null,
87589 dataMinX: 0,
87590 dataMaxX: 1,
87591 dataMinY: 0,
87592 dataMaxY: 1,
87593 centerX: 0,
87594 centerY: 0,
87595 startAngle: 0,
87596 endAngle: Math.PI,
87597 startRho: 0,
87598 endRho: 150,
87599 baseRotation: 0,
87600 labels: null,
87601 labelOverflowPadding: 10
87602 },
87603 dirtyTriggers: {
87604 dataX: 'bbox',
87605 dataY: 'bbox',
87606 dataMinX: 'bbox',
87607 dataMaxX: 'bbox',
87608 dataMinY: 'bbox',
87609 dataMaxY: 'bbox',
87610 centerX: "bbox",
87611 centerY: "bbox",
87612 startAngle: "bbox",
87613 endAngle: "bbox",
87614 startRho: "bbox",
87615 endRho: "bbox",
87616 baseRotation: "bbox"
87617 }
87618 }
87619 },
87620
87621 config: {
87622 /**
87623 * @private
87624 * @cfg {Object} store The store that is passed to the renderer.
87625 */
87626 store: null,
87627 field: null
87628 },
87629
87630 updatePlainBBox: function (plain) {
87631 var attr = this.attr;
87632 plain.x = attr.centerX - attr.endRho;
87633 plain.y = attr.centerY + attr.endRho;
87634 plain.width = attr.endRho * 2;
87635 plain.height = attr.endRho * 2;
87636 }
87637 });
87638
87639 /**
87640 * @class Ext.chart.series.sprite.Radar
87641 * @extends Ext.chart.series.sprite.Polar
87642 *
87643 * Radar series sprite.
87644 */
87645 Ext.define('Ext.chart.series.sprite.Radar', {
87646 alias: 'sprite.radar',
87647 extend: Ext.chart.series.sprite.Polar ,
87648
87649 render: function (surface, ctx) {
87650 var me = this,
87651 attr = me.attr,
87652 centerX = attr.centerX,
87653 centerY = attr.centerY,
87654 matrix = attr.matrix,
87655 minX = attr.dataMinX,
87656 maxX = attr.dataMaxX,
87657 maxY = attr.dataMaxY,
87658 dataX = attr.dataX,
87659 dataY = attr.dataY,
87660 rangeY = attr.rangeY,
87661 endRho = attr.endRho,
87662 startRho = attr.startRho,
87663 baseRotation = attr.baseRotation,
87664 i, length = dataX.length,
87665 markerCfg = {},
87666 surfaceMatrix = me.surfaceMatrix,
87667 x, y, r, th;
87668 ctx.beginPath();
87669 for (i = 0; i < length; i++) {
87670 th = (dataX[i] - minX) / (maxX - minX + 1) * 2 * Math.PI + baseRotation;
87671 r = dataY[i] / (rangeY ? rangeY[1] : maxY) * (endRho - startRho) + startRho;
87672 x = matrix.x(centerX + Math.cos(th) * r, centerY + Math.sin(th) * r);
87673 y = matrix.y(centerX + Math.cos(th) * r, centerY + Math.sin(th) * r);
87674 ctx.lineTo(x, y);
87675 markerCfg.translationX = surfaceMatrix.x(x, y);
87676 markerCfg.translationY = surfaceMatrix.y(x, y);
87677 me.putMarker('markers', markerCfg, i, true);
87678 }
87679 ctx.closePath();
87680 ctx.fillStroke(attr);
87681 }
87682 });
87683
87684 /**
87685 * @class Ext.chart.series.Radar
87686 * @extends Ext.chart.series.Polar
87687 *
87688 * Creates a Radar Chart. A Radar Chart is a useful visualization technique for comparing different quantitative values for
87689 * a constrained number of categories.
87690 * As with all other series, the Radar series must be appended in the *series* Chart array configuration. See the Chart
87691 * documentation for more information. A typical configuration object for the radar series could be:
87692 *
87693 * @example preview
87694 * var chart = new Ext.chart.PolarChart({
87695 * animate: true,
87696 * interactions: ['rotate'],
87697 * store: {
87698 * fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
87699 * data: [
87700 * {'name':'metric one', 'data1':10, 'data2':12, 'data3':14, 'data4':8, 'data5':13},
87701 * {'name':'metric two', 'data1':7, 'data2':8, 'data3':16, 'data4':10, 'data5':3},
87702 * {'name':'metric three', 'data1':5, 'data2':2, 'data3':14, 'data4':12, 'data5':7},
87703 * {'name':'metric four', 'data1':2, 'data2':14, 'data3':6, 'data4':1, 'data5':23},
87704 * {'name':'metric five', 'data1':27, 'data2':38, 'data3':36, 'data4':13, 'data5':33}
87705 * ]
87706 * },
87707 * series: [{
87708 * type: 'radar',
87709 * xField: 'name',
87710 * yField: 'data4',
87711 * style: {
87712 * fillStyle: 'rgba(0, 0, 255, 0.1)',
87713 * strokeStyle: 'rgba(0, 0, 0, 0.8)',
87714 * lineWidth: 1
87715 * }
87716 * }],
87717 * axes: [
87718 * {
87719 * type: 'numeric',
87720 * position: 'radial',
87721 * fields: 'data4',
87722 * style: {
87723 * estStepSize: 10
87724 * },
87725 * grid: true
87726 * },
87727 * {
87728 * type: 'category',
87729 * position: 'angular',
87730 * fields: 'name',
87731 * style: {
87732 * estStepSize: 1
87733 * },
87734 * grid: true
87735 * }
87736 * ]
87737 * });
87738 * Ext.Viewport.setLayout('fit');
87739 * Ext.Viewport.add(chart);
87740 *
87741 *
87742 */
87743 Ext.define('Ext.chart.series.Radar', {
87744 extend: Ext.chart.series.Polar ,
87745 type: "radar",
87746 seriesType: 'radar',
87747 alias: 'series.radar',
87748
87749 /**
87750 * @cfg {Object} style
87751 * An object containing styles for overriding series styles from theming.
87752 */
87753
87754 config: {
87755
87756 },
87757
87758 updateAngularAxis: function (axis) {
87759 axis.processData(this);
87760 },
87761
87762 updateRadialAxis: function (axis) {
87763 axis.processData(this);
87764 },
87765
87766 coordinateX: function () {
87767 return this.coordinate('X', 0, 2);
87768 },
87769
87770 coordinateY: function () {
87771 return this.coordinate('Y', 1, 2);
87772 },
87773
87774 updateCenter: function (center) {
87775 this.setStyle({
87776 translationX: center[0] + this.getOffsetX(),
87777 translationY: center[1] + this.getOffsetY()
87778 });
87779 this.doUpdateStyles();
87780 },
87781
87782 updateRadius: function (radius) {
87783 this.setStyle({
87784 endRho: radius
87785 });
87786 this.doUpdateStyles();
87787 },
87788
87789 updateRotation: function (rotation) {
87790 this.setStyle({
87791 rotationRads: rotation
87792 });
87793 this.doUpdateStyles();
87794 },
87795
87796 updateTotalAngle: function (totalAngle) {
87797 this.processData();
87798 },
87799
87800 getItemForPoint: function (x, y) {
87801 var me = this,
87802 sprite = me.sprites && me.sprites[0],
87803 attr = sprite.attr,
87804 dataX = attr.dataX,
87805 dataY = attr.dataY,
87806 centerX = attr.centerX,
87807 centerY = attr.centerY,
87808 minX = attr.dataMinX,
87809 maxX = attr.dataMaxX,
87810 maxY = attr.dataMaxY,
87811 endRho = attr.endRho,
87812 startRho = attr.startRho,
87813 baseRotation = attr.baseRotation,
87814 i, length = dataX.length,
87815 store = me.getStore(),
87816 marker = me.getMarker(),
87817 item, th, r;
87818
87819 if(me.getHidden()) {
87820 return null;
87821 }
87822 if (sprite && marker) {
87823 for (i = 0; i < length; i++) {
87824 th = (dataX[i] - minX) / (maxX - minX + 1) * 2 * Math.PI + baseRotation;
87825 r = dataY[i] / maxY * (endRho - startRho) + startRho;
87826 if (Math.abs(centerX + Math.cos(th) * r - x) < 22 && Math.abs(centerY + Math.sin(th) * r - y) < 22) {
87827 item = {
87828 series: this,
87829 sprite: sprite,
87830 index: i,
87831 record: store.getData().items[i],
87832 field: store.getFields().items[i]
87833 };
87834 return item;
87835 }
87836 }
87837 }
87838 return this.callSuper(arguments);
87839 },
87840
87841 getXRange: function () {
87842 return [this.dataRange[0], this.dataRange[2]];
87843 },
87844
87845 getYRange: function () {
87846 return [this.dataRange[1], this.dataRange[3]];
87847 }
87848 }, function () {
87849 var klass = this;
87850 // TODO: [HACK] Steal from cartesian series.
87851 klass.prototype.onAxesChanged = Ext.chart.series.Cartesian.prototype.onAxesChanged;
87852 klass.prototype.getSprites = Ext.chart.series.Cartesian.prototype.getSprites;
87853 });
87854
87855
87856 /**
87857 * @class Ext.chart.series.sprite.Scatter
87858 * @extends Ext.chart.series.sprite.Cartesian
87859 *
87860 * Scatter series sprite.
87861 */
87862 Ext.define("Ext.chart.series.sprite.Scatter", {
87863 alias: 'sprite.scatterSeries',
87864 extend: Ext.chart.series.sprite.Cartesian ,
87865 renderClipped: function (surface, ctx, clip, clipRegion) {
87866 if (this.cleanRedraw) {
87867 return;
87868 }
87869 var attr = this.attr,
87870 dataX = attr.dataX,
87871 dataY = attr.dataY,
87872 matrix = this.attr.matrix,
87873 xx = matrix.getXX(),
87874 yy = matrix.getYY(),
87875 dx = matrix.getDX(),
87876 dy = matrix.getDY(),
87877 markerCfg = {},
87878 left = clipRegion[0] - xx,
87879 right = clipRegion[0] + clipRegion[2] + xx,
87880 top = clipRegion[1] - yy,
87881 bottom = clipRegion[1] + clipRegion[3] + yy,
87882 x, y;
87883 for (var i = 0; i < dataX.length; i++) {
87884 x = dataX[i];
87885 y = dataY[i];
87886 x = x * xx + dx;
87887 y = y * yy + dy;
87888 if (left <= x && x <= right && top <= y && y <= bottom) {
87889 if (attr.renderer) {
87890 attr.renderer.call(this, this, markerCfg, {store:this.getStore()}, i);
87891 }
87892 markerCfg.translationX = x;
87893 markerCfg.translationY = y;
87894 this.putMarker("items", markerCfg, i, !attr.renderer);
87895 }
87896 }
87897 }
87898 });
87899
87900 /**
87901 * @class Ext.chart.series.Scatter
87902 * @extends Ext.chart.series.Cartesian
87903 *
87904 * Creates a Scatter Chart. The scatter plot is useful when trying to display more than two variables in the same visualization.
87905 * These variables can be mapped into x, y coordinates and also to an element's radius/size, color, etc.
87906 * As with all other series, the Scatter Series must be appended in the *series* Chart array configuration. See the Chart
87907 * documentation for more information on creating charts. A typical configuration object for the scatter could be:
87908 *
87909 * @example preview
87910 * var chart = new Ext.chart.CartesianChart({
87911 * animate: true,
87912 * store: {
87913 * fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
87914 * data: [
87915 * {'name':'metric one', 'data1':10, 'data2':12, 'data3':14, 'data4':8, 'data5':13},
87916 * {'name':'metric two', 'data1':7, 'data2':8, 'data3':16, 'data4':10, 'data5':3},
87917 * {'name':'metric three', 'data1':5, 'data2':2, 'data3':14, 'data4':12, 'data5':7},
87918 * {'name':'metric four', 'data1':2, 'data2':14, 'data3':6, 'data4':1, 'data5':23},
87919 * {'name':'metric five', 'data1':27, 'data2':38, 'data3':36, 'data4':13, 'data5':33}
87920 * ]
87921 * },
87922 * axes: [{
87923 * type: 'numeric',
87924 * position: 'left',
87925 * fields: ['data1'],
87926 * title: {
87927 * text: 'Sample Values',
87928 * fontSize: 15
87929 * },
87930 * grid: true,
87931 * minimum: 0
87932 * }, {
87933 * type: 'category',
87934 * position: 'bottom',
87935 * fields: ['name'],
87936 * title: {
87937 * text: 'Sample Values',
87938 * fontSize: 15
87939 * }
87940 * }],
87941 * series: [{
87942 * type: 'scatter',
87943 * highlight: {
87944 * size: 7,
87945 * radius: 7
87946 * },
87947 * fill: true,
87948 * xField: 'name',
87949 * yField: 'data3',
87950 * marker: {
87951 * type: 'circle',
87952 * fillStyle: 'blue',
87953 * radius: 10,
87954 * lineWidth: 0
87955 * }
87956 * }]
87957 * });
87958 * Ext.Viewport.setLayout('fit');
87959 * Ext.Viewport.add(chart);
87960 *
87961 * In this configuration we add three different categories of scatter series. Each of them is bound to a different field of the same data store,
87962 * `data1`, `data2` and `data3` respectively. All x-fields for the series must be the same field, in this case `name`.
87963 * Each scatter series has a different styling configuration for markers, specified by the `marker` object. Finally we set the left axis as
87964 * axis to show the current values of the elements.
87965 *
87966 */
87967 Ext.define('Ext.chart.series.Scatter', {
87968
87969 extend: Ext.chart.series.Cartesian ,
87970
87971 alias: 'series.scatter',
87972
87973 type: 'scatter',
87974 seriesType: 'scatterSeries',
87975
87976
87977
87978
87979
87980 config: {
87981 itemInstancing: {
87982 fx: {
87983 customDuration: {
87984 translationX: 0,
87985 translationY: 0
87986 }
87987 }
87988 }
87989 },
87990
87991 applyMarker: function (marker) {
87992 this.getItemInstancing();
87993 this.setItemInstancing(marker);
87994 },
87995
87996 provideLegendInfo: function (target) {
87997 var style = this.config.marker;
87998 target.push({
87999 name: this.getTitle() || this.getYField() || this.getId(),
88000 mark: style.fill || style.stroke || 'black',
88001 disabled: false,
88002 series: this.getId(),
88003 index: 0
88004 });
88005 }
88006 });
88007
88008
88009 /**
88010 * @author Ed Spencer
88011 *
88012 * Small helper class to make creating {@link Ext.data.Store}s from Array data easier. An ArrayStore will be
88013 * automatically configured with a {@link Ext.data.reader.Array}.
88014 *
88015 * A store configuration would be something like:
88016 *
88017 * var store = Ext.create('Ext.data.ArrayStore', {
88018 * // store configs
88019 * autoDestroy: true,
88020 * storeId: 'myStore',
88021 * // reader configs
88022 * idIndex: 0,
88023 * fields: [
88024 * 'company',
88025 * {name: 'price', type: 'float'},
88026 * {name: 'change', type: 'float'},
88027 * {name: 'pctChange', type: 'float'},
88028 * {name: 'lastChange', type: 'date', dateFormat: 'n/j h:ia'}
88029 * ]
88030 * });
88031 *
88032 * This store is configured to consume a returned object of the form:
88033 *
88034 * var myData = [
88035 * ['3m Co',71.72,0.02,0.03,'9/1 12:00am'],
88036 * ['Alcoa Inc',29.01,0.42,1.47,'9/1 12:00am'],
88037 * ['Boeing Co.',75.43,0.53,0.71,'9/1 12:00am'],
88038 * ['Hewlett-Packard Co.',36.53,-0.03,-0.08,'9/1 12:00am'],
88039 * ['Wal-Mart Stores, Inc.',45.45,0.73,1.63,'9/1 12:00am']
88040 * ];
88041 *
88042 * An object literal of this form could also be used as the {@link #data} config option.
88043 *
88044 * **Note:** Although not listed here, this class accepts all of the configuration options of
88045 * **{@link Ext.data.reader.Array ArrayReader}**.
88046 *
88047 * ###Further Reading
88048 * [Sencha Touch Data Overview](../../../core_concepts/data/data_package_overview.html)
88049 * [Sencha Touch Store Guide](../../../core_concepts/data/stores.html)
88050 * [Sencha Touch Models Guide](../../../core_concepts/data/models.html)
88051 * [Sencha Touch Proxy Guide](../../../core_concepts/data/proxies.html)
88052 */
88053 Ext.define('Ext.data.ArrayStore', {
88054 extend: Ext.data.Store ,
88055 alias: 'store.array',
88056
88057
88058 config: {
88059 proxy: {
88060 type: 'memory',
88061 reader: 'array'
88062 }
88063 },
88064
88065 loadData: function(data, append) {
88066 // if (this.expandData === true) {
88067 // var r = [],
88068 // i = 0,
88069 // ln = data.length;
88070 //
88071 // for (; i < ln; i++) {
88072 // r[r.length] = [data[i]];
88073 // }
88074 //
88075 // data = r;
88076 // }
88077
88078 this.callParent([data, append]);
88079 }
88080 }, function() {
88081 // backwards compat
88082 Ext.data.SimpleStore = Ext.data.ArrayStore;
88083 // Ext.reg('simplestore', Ext.data.SimpleStore);
88084 });
88085
88086 /**
88087 * Ext.Direct aims to streamline communication between the client and server by providing a single interface that
88088 * reduces the amount of common code typically required to validate data and handle returned data packets (reading data,
88089 * error conditions, etc).
88090 *
88091 * The Ext.direct namespace includes several classes for a closer integration with the server-side. The Ext.data
88092 * namespace also includes classes for working with Ext.data.Stores which are backed by data from an Ext.Direct method.
88093 *
88094 * # Specification
88095 *
88096 * For additional information consult the [Ext.Direct Specification](http://sencha.com/products/extjs/extdirect).
88097 *
88098 * # Providers
88099 *
88100 * Ext.Direct uses a provider architecture, where one or more providers are used to transport data to and from the
88101 * server. There are several providers that exist in the core at the moment:
88102 *
88103 * - {@link Ext.direct.JsonProvider JsonProvider} for simple JSON operations
88104 * - {@link Ext.direct.PollingProvider PollingProvider} for repeated requests
88105 * - {@link Ext.direct.RemotingProvider RemotingProvider} exposes server side on the client.
88106 *
88107 * A provider does not need to be invoked directly, providers are added via {@link Ext.direct.Manager}.{@link #addProvider}.
88108 *
88109 * # Router
88110 *
88111 * Ext.Direct utilizes a "router" on the server to direct requests from the client to the appropriate server-side
88112 * method. Because the Ext.Direct API is completely platform-agnostic, you could completely swap out a Java based server
88113 * solution and replace it with one that uses C# without changing the client side JavaScript at all.
88114 *
88115 * # Server side events
88116 *
88117 * Custom events from the server may be handled by the client by adding listeners, for example:
88118 *
88119 * {"type":"event","name":"message","data":"Successfully polled at: 11:19:30 am"}
88120 *
88121 * // add a handler for a 'message' event sent by the server
88122 * Ext.direct.Manager.on('message', function(e){
88123 * out.append(String.format('<p><i>{0}</i></p>', e.data));
88124 * out.el.scrollTo('t', 100000, true);
88125 * });
88126 *
88127 * @singleton
88128 * @alternateClassName Ext.Direct
88129 */
88130 Ext.define('Ext.direct.Manager', {
88131 singleton: true,
88132
88133 mixins: {
88134 observable: Ext.mixin.Observable
88135 },
88136
88137
88138
88139 alternateClassName: 'Ext.Direct',
88140
88141 exceptions: {
88142 TRANSPORT: 'xhr',
88143 PARSE: 'parse',
88144 LOGIN: 'login',
88145 SERVER: 'exception'
88146 },
88147
88148 /**
88149 * @event event
88150 * Fires after an event.
88151 * @param {Ext.direct.Event} e The Ext.direct.Event type that occurred.
88152 * @param {Ext.direct.Provider} provider The {@link Ext.direct.Provider Provider}.
88153 */
88154
88155 /**
88156 * @event exception
88157 * Fires after an event exception.
88158 * @param {Ext.direct.Event} e The event type that occurred.
88159 */
88160
88161 constructor: function() {
88162 var me = this;
88163
88164 me.transactions = Ext.create('Ext.util.Collection', this.getKey);
88165 me.providers = Ext.create('Ext.util.Collection', this.getKey);
88166 },
88167
88168 getKey: function(item) {
88169 return item.getId();
88170 },
88171
88172 /**
88173 * Adds an Ext.Direct Provider and creates the proxy or stub methods to execute server-side methods. If the provider
88174 * is not already connected, it will auto-connect.
88175 *
88176 * Ext.direct.Manager.addProvider({
88177 * type: "remoting", // create a {@link Ext.direct.RemotingProvider}
88178 * url: "php/router.php", // url to connect to the Ext.Direct server-side router.
88179 * actions: { // each property within the actions object represents a Class
88180 * TestAction: [ // array of methods within each server side Class
88181 * {
88182 * name: "doEcho", // name of method
88183 * len: 1
88184 * },{
88185 * name: "multiply",
88186 * len: 1
88187 * },{
88188 * name: "doForm",
88189 * formHandler: true, // handle form on server with Ext.Direct.Transaction
88190 * len: 1
88191 * }]
88192 * },
88193 * namespace: "myApplication" // namespace to create the Remoting Provider in
88194 * });
88195 *
88196 * @param {Ext.direct.Provider/Object...} provider
88197 * Accepts any number of Provider descriptions (an instance or config object for
88198 * a Provider). Each Provider description instructs Ext.Direct how to create
88199 * client-side stub methods.
88200 * @return {Object}
88201 */
88202 addProvider : function(provider) {
88203 var me = this,
88204 args = Ext.toArray(arguments),
88205 i = 0, ln;
88206
88207 if (args.length > 1) {
88208 for (ln = args.length; i < ln; ++i) {
88209 me.addProvider(args[i]);
88210 }
88211 return;
88212 }
88213
88214 // if provider has not already been instantiated
88215 if (!provider.isProvider) {
88216 provider = Ext.create('direct.' + provider.type + 'provider', provider);
88217 }
88218 me.providers.add(provider);
88219 provider.on('data', me.onProviderData, me);
88220
88221 if (!provider.isConnected()) {
88222 provider.connect();
88223 }
88224
88225 return provider;
88226 },
88227
88228 /**
88229 * Retrieves a {@link Ext.direct.Provider provider} by the **{@link Ext.direct.Provider#id id}** specified when the
88230 * provider is {@link #addProvider added}.
88231 * @param {String/Ext.direct.Provider} id The id of the provider, or the provider instance.
88232 * @return {Object}
88233 */
88234 getProvider : function(id){
88235 return id.isProvider ? id : this.providers.get(id);
88236 },
88237
88238 /**
88239 * Removes the provider.
88240 * @param {String/Ext.direct.Provider} provider The provider instance or the id of the provider.
88241 * @return {Ext.direct.Provider/null} The provider, `null` if not found.
88242 */
88243 removeProvider : function(provider) {
88244 var me = this,
88245 providers = me.providers;
88246
88247 provider = provider.isProvider ? provider : providers.get(provider);
88248
88249 if (provider) {
88250 provider.un('data', me.onProviderData, me);
88251 providers.remove(provider);
88252 return provider;
88253 }
88254 return null;
88255 },
88256
88257 /**
88258 * Adds a transaction to the manager.
88259 * @private
88260 * @param {Ext.direct.Transaction} transaction The transaction to add
88261 * @return {Ext.direct.Transaction} transaction
88262 */
88263 addTransaction: function(transaction) {
88264 this.transactions.add(transaction);
88265 return transaction;
88266 },
88267
88268 /**
88269 * Removes a transaction from the manager.
88270 * @private
88271 * @param {String/Ext.direct.Transaction} transaction The transaction/id of transaction to remove
88272 * @return {Ext.direct.Transaction} transaction
88273 */
88274 removeTransaction: function(transaction) {
88275 transaction = this.getTransaction(transaction);
88276 this.transactions.remove(transaction);
88277 return transaction;
88278 },
88279
88280 /**
88281 * Gets a transaction
88282 * @private
88283 * @param {String/Ext.direct.Transaction} transaction The transaction/id of transaction to get
88284 * @return {Ext.direct.Transaction}
88285 */
88286 getTransaction: function(transaction) {
88287 return Ext.isObject(transaction) ? transaction : this.transactions.get(transaction);
88288 },
88289
88290 onProviderData : function(provider, event) {
88291 var me = this,
88292 i = 0, ln,
88293 name;
88294
88295 if (Ext.isArray(event)) {
88296 for (ln = event.length; i < ln; ++i) {
88297 me.onProviderData(provider, event[i]);
88298 }
88299 return;
88300 }
88301
88302 name = event.getName();
88303
88304 if (name && name != 'event' && name != 'exception') {
88305 me.fireEvent(name, event);
88306 } else if (event.getStatus() === false) {
88307 me.fireEvent('exception', event);
88308 }
88309
88310 me.fireEvent('event', event, provider);
88311 },
88312
88313 /**
88314 * Parses a direct function. It may be passed in a string format, for example:
88315 * "MyApp.Person.read".
88316 * @protected
88317 * @param {String/Function} fn The direct function
88318 * @return {Function} The function to use in the direct call. Null if not found
88319 */
88320 parseMethod: function(fn) {
88321 if (Ext.isString(fn)) {
88322 var parts = fn.split('.'),
88323 i = 0,
88324 ln = parts.length,
88325 current = window;
88326
88327 while (current && i < ln) {
88328 current = current[parts[i]];
88329 ++i;
88330 }
88331 fn = Ext.isFunction(current) ? current : null;
88332 }
88333 return fn || null;
88334 }
88335 });
88336
88337 /**
88338 * This class is used to send requests to the server using {@link Ext.direct.Manager Ext.Direct}. When a
88339 * request is made, the transport mechanism is handed off to the appropriate
88340 * {@link Ext.direct.RemotingProvider Provider} to complete the call.
88341 *
88342 * # Specifying the function
88343 *
88344 * This proxy expects a Direct remoting method to be passed in order to be able to complete requests.
88345 * This can be done by specifying the {@link #directFn} configuration. This will use the same direct
88346 * method for all requests. Alternatively, you can provide an {@link #api} configuration. This
88347 * allows you to specify a different remoting method for each CRUD action.
88348 *
88349 * # Parameters
88350 *
88351 * This proxy provides options to help configure which parameters will be sent to the server.
88352 * By specifying the {@link #paramsAsHash} option, it will send an object literal containing each
88353 * of the passed parameters. The {@link #paramOrder} option can be used to specify the order in which
88354 * the remoting method parameters are passed.
88355 *
88356 * # Example Usage
88357 *
88358 * Ext.define('User', {
88359 * extend: 'Ext.data.Model',
88360 * config: {
88361 * fields: ['firstName', 'lastName'],
88362 * proxy: {
88363 * type: 'direct',
88364 * directFn: MyApp.getUsers,
88365 * paramOrder: 'id' // Tells the proxy to pass the id as the first parameter to the remoting method.
88366 * }
88367 * }
88368 * });
88369 * User.load(1);
88370 *
88371 * ###Further Reading
88372 * [Sencha Touch Data Overview](../../../core_concepts/data/data_package_overview.html)
88373 * [Sencha Touch Store Guide](../../../core_concepts/data/stores.html)
88374 * [Sencha Touch Models Guide](../../../core_concepts/data/models.html)
88375 * [Sencha Touch Proxy Guide](../../../core_concepts/data/proxies.html)
88376 */
88377 Ext.define('Ext.data.proxy.Direct', {
88378 extend: Ext.data.proxy.Server ,
88379 alternateClassName: 'Ext.data.DirectProxy',
88380 alias: 'proxy.direct',
88381
88382
88383 config: {
88384 /**
88385 * @cfg url
88386 * @hide
88387 */
88388
88389 /**
88390 * @cfg {String/String[]} paramOrder
88391 * Defaults to undefined. A list of params to be executed server side. Specify the params in the order in
88392 * which they must be executed on the server-side as either (1) an Array of String values, or (2) a String
88393 * of params delimited by either whitespace, comma, or pipe. For example, any of the following would be
88394 * acceptable:
88395 *
88396 * paramOrder: ['param1','param2','param3']
88397 * paramOrder: 'param1 param2 param3'
88398 * paramOrder: 'param1,param2,param3'
88399 * paramOrder: 'param1|param2|param'
88400 */
88401 paramOrder: undefined,
88402
88403 /**
88404 * @cfg {Boolean} paramsAsHash
88405 * Send parameters as a collection of named arguments.
88406 * Providing a {@link #paramOrder} nullifies this configuration.
88407 */
88408 paramsAsHash: true,
88409
88410 /**
88411 * @cfg {Function/String} directFn
88412 * Function to call when executing a request. directFn is a simple alternative to defining the api configuration-parameter
88413 * for Store's which will not implement a full CRUD api. The directFn may also be a string reference to the fully qualified
88414 * name of the function, for example: 'MyApp.company.GetProfile'. This can be useful when using dynamic loading. The string
88415 * will be looked up when the proxy is created.
88416 */
88417 directFn : undefined,
88418
88419 /**
88420 * @cfg {Object} api
88421 * The same as {@link Ext.data.proxy.Server#api}, however instead of providing urls, you should provide a direct
88422 * function call. See {@link #directFn}.
88423 */
88424 api: null,
88425
88426 /**
88427 * @cfg {Object} extraParams
88428 * Extra parameters that will be included on every read request. Individual requests with params
88429 * of the same name will override these params when they are in conflict.
88430 */
88431 extraParams: null
88432 },
88433
88434 // @private
88435 paramOrderRe: /[\s,|]/,
88436
88437 applyParamOrder: function(paramOrder) {
88438 if (Ext.isString(paramOrder)) {
88439 paramOrder = paramOrder.split(this.paramOrderRe);
88440 }
88441 return paramOrder;
88442 },
88443
88444 resolveMethods : function() {
88445 var me = this,
88446 fn = me.getDirectFn(),
88447 api = me.getApi(),
88448 Manager = Ext.direct.Manager,
88449 method;
88450
88451 if (fn) {
88452 me.setDirectFn(method = Manager.parseMethod(fn));
88453
88454 if (!Ext.isFunction(method)) {
88455 Ext.Error.raise('Cannot resolve directFn ' + fn);
88456 }
88457 }
88458 else if (api) {
88459 for (fn in api) {
88460 if (api.hasOwnProperty(fn)) {
88461 method = api[fn];
88462 api[fn] = Manager.parseMethod(method);
88463
88464 if (!Ext.isFunction(api[fn])) {
88465 Ext.Error.raise('Cannot resolve Direct api ' + fn + ' method ' + method);
88466 }
88467 }
88468 }
88469 }
88470
88471 me.methodsResolved = true;
88472 },
88473
88474 doRequest: function(operation, callback, scope) {
88475 var me = this,
88476 writer = me.getWriter(),
88477 request = me.buildRequest(operation, callback, scope),
88478 api = me.getApi() || {},
88479 params = request.getParams(),
88480 args = [],
88481 fn, method;
88482
88483 if (!me.methodsResolved) {
88484 me.resolveMethods();
88485 }
88486
88487 fn = api[request.getAction()] || me.getDirectFn();
88488
88489 //<debug>
88490 if (!fn) {
88491 Ext.Logger.error('No direct function specified for this proxy');
88492 }
88493 //</debug>
88494
88495 request = writer.write(request);
88496
88497 if (operation.getAction() == 'read') {
88498 // We need to pass params
88499 method = fn.directCfg.method;
88500 args = method.getArgs(params, me.getParamOrder(), me.getParamsAsHash());
88501 } else {
88502 args.push(request.getJsonData());
88503 }
88504
88505 args.push(me.createRequestCallback(request, operation, callback, scope), me);
88506
88507 request.setConfig({
88508 args: args,
88509 directFn: fn
88510 });
88511
88512 fn.apply(window, args);
88513 },
88514
88515 /*
88516 * Inherit docs. We don't apply any encoding here because
88517 * all of the direct requests go out as jsonData
88518 */
88519 applyEncoding: function(value) {
88520 return value;
88521 },
88522
88523 createRequestCallback: function(request, operation, callback, scope) {
88524 var me = this;
88525
88526 return function(data, event) {
88527 me.processResponse(event.getStatus(), operation, request, event, callback, scope);
88528 };
88529 },
88530
88531 getResponseResult: function(response) {
88532 return response.getResult();
88533 },
88534
88535 // @inheritdoc
88536 extractResponseData: function(response) {
88537 var result = response.getResult();
88538 return Ext.isDefined(result) ? result : response.getData();
88539 },
88540
88541 // @inheritdoc
88542 setException: function(operation, response) {
88543 operation.setException(response.getMessage());
88544 },
88545
88546 // @inheritdoc
88547 buildUrl: function() {
88548 return '';
88549 }
88550 });
88551
88552 /**
88553 * Small helper class to create an {@link Ext.data.Store} configured with an {@link Ext.data.proxy.Direct}
88554 * and {@link Ext.data.reader.Json} to make interacting with an {@link Ext.direct.Manager} server-side
88555 * {@link Ext.direct.Provider Provider} easier. To create a different proxy/reader combination create a basic
88556 * {@link Ext.data.Store} configured as needed.
88557 *
88558 * Since configurations are deeply merged with the standard configuration, you can override certain proxy and
88559 * reader configurations like this:
88560 *
88561 * Ext.create('Ext.data.DirectStore', {
88562 * proxy: {
88563 * paramsAsHash: true,
88564 * directFn: someDirectFn,
88565 * simpleSortMode: true,
88566 * reader: {
88567 * rootProperty: 'results',
88568 * idProperty: '_id'
88569 * }
88570 * }
88571 * });
88572 *
88573 * ###Further Reading
88574 * [Sencha Touch Data Overview](../../../core_concepts/data/data_package_overview.html)
88575 * [Sencha Touch Store Guide](../../../core_concepts/data/stores.html)
88576 * [Sencha Touch Models Guide](../../../core_concepts/data/models.html)
88577 * [Sencha Touch Proxy Guide](../../../core_concepts/data/proxies.html)
88578 */
88579 Ext.define('Ext.data.DirectStore', {
88580 extend: Ext.data.Store ,
88581 alias: 'store.direct',
88582
88583
88584 config: {
88585 proxy: {
88586 type: 'direct',
88587 reader: {
88588 type: 'json'
88589 }
88590 }
88591 }
88592 });
88593
88594 /**
88595 * @singleton
88596 *
88597 * This class is used to create JsonP requests. JsonP is a mechanism that allows for making requests for data cross
88598 * domain. More information is available [here](http://en.wikipedia.org/wiki/JSONP).
88599 *
88600 * ## Example
88601 *
88602 * @example preview
88603 * Ext.Viewport.add({
88604 * xtype: 'button',
88605 * text: 'Make JsonP Request',
88606 * centered: true,
88607 * handler: function(button) {
88608 * // Mask the viewport
88609 * Ext.Viewport.mask();
88610 *
88611 * // Remove the button
88612 * button.destroy();
88613 *
88614 * // Make the JsonP request
88615 * Ext.data.JsonP.request({
88616 * url: 'http://free.worldweatheronline.com/feed/weather.ashx',
88617 * callbackKey: 'callback',
88618 * params: {
88619 * key: '23f6a0ab24185952101705',
88620 * q: '94301', // Palo Alto
88621 * format: 'json',
88622 * num_of_days: 5
88623 * },
88624 * success: function(result, request) {
88625 * // Unmask the viewport
88626 * Ext.Viewport.unmask();
88627 *
88628 * // Get the weather data from the json object result
88629 * var weather = result.data.weather;
88630 * if (weather) {
88631 * // Style the viewport html, and set the html of the max temperature
88632 * Ext.Viewport.setStyleHtmlContent(true);
88633 * Ext.Viewport.setHtml('The temperature in Palo Alto is <b>' + weather[0].tempMaxF + '° F</b>');
88634 * }
88635 * }
88636 * });
88637 * }
88638 * });
88639 *
88640 * See the {@link #request} method for more details on making a JsonP request.
88641 *
88642 * ###Further Reading
88643 * [Sencha Touch AJAX Guide](../../../core_concepts/using_ajax.html)
88644 * [Sencha Touch Data Overview](../../../core_concepts/data/data_package_overview.html)
88645 * [Sencha Touch Store Guide](../../../core_concepts/data/stores.html)
88646 * [Sencha Touch Models Guide](../../../core_concepts/data/models.html)
88647 * [Sencha Touch Proxy Guide](../../../core_concepts/data/proxies.html)
88648 *
88649 */
88650 Ext.define('Ext.data.JsonP', {
88651 alternateClassName: 'Ext.util.JSONP',
88652
88653 /* Begin Definitions */
88654
88655 singleton: true,
88656
88657
88658 /* End Definitions */
88659
88660 /**
88661 * Number of requests done so far.
88662 * @private
88663 */
88664 requestCount: 0,
88665
88666 /**
88667 * Hash of pending requests.
88668 * @private
88669 */
88670 requests: {},
88671
88672 /**
88673 * @property {Number} [timeout=30000]
88674 * A default timeout (in milliseconds) for any JsonP requests. If the request has not completed in this time the failure callback will
88675 * be fired.
88676 */
88677 timeout: 30000,
88678
88679 /**
88680 * @property {Boolean} disableCaching
88681 * `true` to add a unique cache-buster param to requests.
88682 */
88683 disableCaching: true,
88684
88685 /**
88686 * @property {String} disableCachingParam
88687 * Change the parameter which is sent went disabling caching through a cache buster.
88688 */
88689 disableCachingParam: '_dc',
88690
88691 /**
88692 * @property {String} callbackKey
88693 * Specifies the GET parameter that will be sent to the server containing the function name to be executed when the
88694 * request completes. Thus, a common request will be in the form of:
88695 * `url?callback=Ext.data.JsonP.callback1`
88696 */
88697 callbackKey: 'callback',
88698
88699 /**
88700 * Makes a JSONP request.
88701 * @param {Object} options An object which may contain the following properties. Note that options will take
88702 * priority over any defaults that are specified in the class.
88703 *
88704 * @param {String} options.url The URL to request.
88705 * @param {Object} [options.params] An object containing a series of key value pairs that will be sent along with the request.
88706 * @param {Number} [options.timeout] See {@link #timeout}
88707 * @param {String} [options.callbackKey] See {@link #callbackKey}
88708 * @param {String} [options.callbackName] See {@link #callbackKey}
88709 * The function name to use for this request. By default this name will be auto-generated: Ext.data.JsonP.callback1,
88710 * Ext.data.JsonP.callback2, etc. Setting this option to "my_name" will force the function name to be
88711 * Ext.data.JsonP.my_name. Use this if you want deterministic behavior, but be careful - the callbackName should be
88712 * different in each JsonP request that you make.
88713 * @param {Boolean} [options.disableCaching] See {@link #disableCaching}
88714 * @param {String} [options.disableCachingParam] See {@link #disableCachingParam}
88715 * @param {Function} [options.success] A function to execute if the request succeeds.
88716 * @param {Function} [options.failure] A function to execute if the request fails.
88717 * @param {Function} [options.callback] A function to execute when the request completes, whether it is a success or failure.
88718 * @param {Object} [options.scope] The scope in which to execute the callbacks: The "this" object for the
88719 * callback function. Defaults to the browser window.
88720 *
88721 * @return {Object} request An object containing the request details.
88722 */
88723 request: function(options){
88724 options = Ext.apply({}, options);
88725
88726 //<debug>
88727 if (!options.url) {
88728 Ext.Logger.error('A url must be specified for a JSONP request.');
88729 }
88730 //</debug>
88731
88732 var me = this,
88733 disableCaching = Ext.isDefined(options.disableCaching) ? options.disableCaching : me.disableCaching,
88734 cacheParam = options.disableCachingParam || me.disableCachingParam,
88735 id = ++me.requestCount,
88736 callbackName = options.callbackName || 'callback' + id,
88737 callbackKey = options.callbackKey || me.callbackKey,
88738 timeout = Ext.isDefined(options.timeout) ? options.timeout : me.timeout,
88739 params = Ext.apply({}, options.params),
88740 url = options.url,
88741 name = Ext.isSandboxed ? Ext.getUniqueGlobalNamespace() : 'Ext',
88742 request,
88743 script;
88744
88745 params[callbackKey] = name + '.data.JsonP.' + callbackName;
88746 if (disableCaching) {
88747 params[cacheParam] = new Date().getTime();
88748 }
88749
88750 script = me.createScript(url, params, options);
88751
88752 me.requests[id] = request = {
88753 url: url,
88754 params: params,
88755 script: script,
88756 id: id,
88757 scope: options.scope,
88758 success: options.success,
88759 failure: options.failure,
88760 callback: options.callback,
88761 callbackKey: callbackKey,
88762 callbackName: callbackName
88763 };
88764
88765 if (timeout > 0) {
88766 request.timeout = setTimeout(Ext.bind(me.handleTimeout, me, [request]), timeout);
88767 }
88768
88769 me.setupErrorHandling(request);
88770 me[callbackName] = Ext.bind(me.handleResponse, me, [request], true);
88771 me.loadScript(request);
88772 return request;
88773 },
88774
88775 /**
88776 * Abort a request. If the request parameter is not specified all open requests will be aborted.
88777 * @param {Object/String} request The request to abort.
88778 */
88779 abort: function(request){
88780 var requests = this.requests,
88781 key;
88782
88783 if (request) {
88784 if (!request.id) {
88785 request = requests[request];
88786 }
88787 this.handleAbort(request);
88788 } else {
88789 for (key in requests) {
88790 if (requests.hasOwnProperty(key)) {
88791 this.abort(requests[key]);
88792 }
88793 }
88794 }
88795 },
88796
88797 /**
88798 * Sets up error handling for the script.
88799 * @private
88800 * @param {Object} request The request.
88801 */
88802 setupErrorHandling: function(request){
88803 request.script.onerror = Ext.bind(this.handleError, this, [request]);
88804 },
88805
88806 /**
88807 * Handles any aborts when loading the script.
88808 * @private
88809 * @param {Object} request The request.
88810 */
88811 handleAbort: function(request){
88812 request.errorType = 'abort';
88813 this.handleResponse(null, request);
88814 },
88815
88816 /**
88817 * Handles any script errors when loading the script.
88818 * @private
88819 * @param {Object} request The request.
88820 */
88821 handleError: function(request){
88822 request.errorType = 'error';
88823 this.handleResponse(null, request);
88824 },
88825
88826 /**
88827 * Cleans up any script handling errors.
88828 * @private
88829 * @param {Object} request The request.
88830 */
88831 cleanupErrorHandling: function(request){
88832 request.script.onerror = null;
88833 },
88834
88835 /**
88836 * Handle any script timeouts.
88837 * @private
88838 * @param {Object} request The request.
88839 */
88840 handleTimeout: function(request){
88841 request.errorType = 'timeout';
88842 this.handleResponse(null, request);
88843 },
88844
88845 /**
88846 * Handle a successful response
88847 * @private
88848 * @param {Object} result The result from the request
88849 * @param {Object} request The request
88850 */
88851 handleResponse: function(result, request){
88852 var success = true;
88853
88854 if (request.timeout) {
88855 clearTimeout(request.timeout);
88856 }
88857
88858 delete this[request.callbackName];
88859 delete this.requests[request.id];
88860
88861 this.cleanupErrorHandling(request);
88862 Ext.fly(request.script).destroy();
88863
88864 if (request.errorType) {
88865 success = false;
88866 Ext.callback(request.failure, request.scope, [request.errorType, request]);
88867 } else {
88868 Ext.callback(request.success, request.scope, [result, request]);
88869 }
88870 Ext.callback(request.callback, request.scope, [success, result, request.errorType, request]);
88871 },
88872
88873 /**
88874 * Create the script tag given the specified url, params and options. The options
88875 * parameter is passed to allow an override to access it.
88876 * @private
88877 * @param {String} url The url of the request
88878 * @param {Object} params Any extra params to be sent
88879 * @param {Object} options The object passed to {@link #request}.
88880 */
88881 createScript: function(url, params, options) {
88882 var script = document.createElement('script');
88883 script.setAttribute("src", Ext.urlAppend(url, Ext.Object.toQueryString(params)));
88884 script.setAttribute("async", true);
88885 script.setAttribute("type", "text/javascript");
88886 return script;
88887 },
88888
88889 /**
88890 * Loads the script for the given request by appending it to the HEAD element. This is
88891 * its own method so that users can override it (as well as {@link #createScript}).
88892 * @private
88893 * @param {Object} request The request object.
88894 */
88895 loadScript: function (request) {
88896 Ext.getHead().appendChild(request.script);
88897 }
88898 });
88899
88900 /**
88901 * @author Ed Spencer
88902 * @class Ext.data.JsonStore
88903 * @extends Ext.data.Store
88904 * @private
88905 *
88906 * Small helper class to make creating {@link Ext.data.Store}s from JSON data easier.
88907 * A JsonStore will be automatically configured with a {@link Ext.data.reader.Json}.
88908 *
88909 * A store configuration would be something like:
88910 *
88911 * var store = new Ext.data.JsonStore({
88912 * // store configs
88913 * autoDestroy: true,
88914 * storeId: 'myStore',
88915 *
88916 * proxy: {
88917 * type: 'ajax',
88918 * url: 'get-images.php',
88919 * reader: {
88920 * type: 'json',
88921 * root: 'images',
88922 * idProperty: 'name'
88923 * }
88924 * },
88925 *
88926 * // alternatively, a {@link Ext.data.Model} name can be given (see {@link Ext.data.Store} for an example)
88927 * fields: ['name', 'url', {name:'size', type: 'float'}, {name:'lastmod', type:'date'}]
88928 * });
88929 *
88930 * This store is configured to consume a returned object of the form:
88931 *
88932 * {
88933 * images: [
88934 * {name: 'Image one', url:'/GetImage.php?id=1', size:46.5, lastmod: new Date(2007, 10, 29)},
88935 * {name: 'Image Two', url:'/GetImage.php?id=2', size:43.2, lastmod: new Date(2007, 10, 30)}
88936 * ]
88937 * }
88938 *
88939 * An object literal of this form could also be used as the {@link #data} config option.
88940 *
88941 * @xtype jsonstore
88942 */
88943 Ext.define('Ext.data.JsonStore', {
88944 extend: Ext.data.Store ,
88945 alias: 'store.json',
88946
88947 config: {
88948 proxy: {
88949 type: 'ajax',
88950 reader: 'json',
88951 writer: 'json'
88952 }
88953 }
88954 });
88955
88956 /**
88957 * @class Ext.data.NodeInterface
88958 * This class is meant to be used as a set of methods that are applied to the prototype of a
88959 * Record to decorate it with a Node API. This means that models used in conjunction with a tree
88960 * will have all of the tree related methods available on the model. In general this class will
88961 * not be used directly by the developer. This class also creates extra fields on the model if
88962 * they do not exist, to help maintain the tree state and UI. These fields are:
88963 *
88964 * - parentId
88965 * - index
88966 * - depth
88967 * - expanded
88968 * - expandable
88969 * - checked
88970 * - leaf
88971 * - cls
88972 * - iconCls
88973 * - root
88974 * - isLast
88975 * - isFirst
88976 * - allowDrop
88977 * - allowDrag
88978 * - loaded
88979 * - loading
88980 * - href
88981 * - hrefTarget
88982 * - qtip
88983 * - qtitle
88984 */
88985 Ext.define('Ext.data.NodeInterface', {
88986
88987
88988 alternateClassName: 'Ext.data.Node',
88989
88990 /**
88991 * @property nextSibling
88992 * A reference to this node's next sibling node. `null` if this node does not have a next sibling.
88993 */
88994
88995 /**
88996 * @property previousSibling
88997 * A reference to this node's previous sibling node. `null` if this node does not have a previous sibling.
88998 */
88999
89000 /**
89001 * @property parentNode
89002 * A reference to this node's parent node. `null` if this node is the root node.
89003 */
89004
89005 /**
89006 * @property lastChild
89007 * A reference to this node's last child node. `null` if this node has no children.
89008 */
89009
89010 /**
89011 * @property firstChild
89012 * A reference to this node's first child node. `null` if this node has no children.
89013 */
89014
89015 /**
89016 * @property childNodes
89017 * An array of this nodes children. Array will be empty if this node has no children.
89018 */
89019
89020 statics: {
89021 /**
89022 * This method allows you to decorate a Record's prototype to implement the NodeInterface.
89023 * This adds a set of methods, new events, new properties and new fields on every Record
89024 * with the same Model as the passed Record.
89025 * @param {Ext.data.Model} record The Record you want to decorate the prototype of.
89026 * @static
89027 */
89028 decorate: function(record) {
89029 if (!record.isNode) {
89030 // Apply the methods and fields to the prototype
89031 var mgr = Ext.data.ModelManager,
89032 modelName = record.modelName,
89033 modelClass = mgr.getModel(modelName),
89034 newFields = [],
89035 i, newField, len;
89036
89037 // Start by adding the NodeInterface methods to the Model's prototype
89038 modelClass.override(this.getPrototypeBody());
89039
89040 newFields = this.applyFields(modelClass, [
89041 {name: 'parentId', type: 'string', defaultValue: null},
89042 {name: 'index', type: 'int', defaultValue: 0},
89043 {name: 'depth', type: 'int', defaultValue: 0, persist: false},
89044 {name: 'expanded', type: 'bool', defaultValue: false, persist: false},
89045 {name: 'expandable', type: 'bool', defaultValue: true, persist: false},
89046 {name: 'checked', type: 'auto', defaultValue: null},
89047 {name: 'leaf', type: 'bool', defaultValue: false, persist: false},
89048 {name: 'cls', type: 'string', defaultValue: null, persist: false},
89049 {name: 'iconCls', type: 'string', defaultValue: null, persist: false},
89050 {name: 'root', type: 'boolean', defaultValue: false, persist: false},
89051 {name: 'isLast', type: 'boolean', defaultValue: false, persist: false},
89052 {name: 'isFirst', type: 'boolean', defaultValue: false, persist: false},
89053 {name: 'allowDrop', type: 'boolean', defaultValue: true, persist: false},
89054 {name: 'allowDrag', type: 'boolean', defaultValue: true, persist: false},
89055 {name: 'loaded', type: 'boolean', defaultValue: false, persist: false},
89056 {name: 'loading', type: 'boolean', defaultValue: false, persist: false},
89057 {name: 'href', type: 'string', defaultValue: null, persist: false},
89058 {name: 'hrefTarget', type: 'string', defaultValue: null, persist: false},
89059 {name: 'qtip', type: 'string', defaultValue: null, persist: false},
89060 {name: 'qtitle', type: 'string', defaultValue: null, persist: false}
89061 ]);
89062
89063 len = newFields.length;
89064
89065 // We set a dirty flag on the fields collection of the model. Any reader that
89066 // will read in data for this model will update their extractor functions.
89067 modelClass.getFields().isDirty = true;
89068
89069 // Set default values
89070 for (i = 0; i < len; ++i) {
89071 newField = newFields[i];
89072 if (record.get(newField.getName()) === undefined) {
89073 record.data[newField.getName()] = newField.getDefaultValue();
89074 }
89075 }
89076 }
89077
89078 if (!record.isDecorated) {
89079 record.isDecorated = true;
89080
89081 Ext.applyIf(record, {
89082 firstChild: null,
89083 lastChild: null,
89084 parentNode: null,
89085 previousSibling: null,
89086 nextSibling: null,
89087 childNodes: []
89088 });
89089
89090 record.enableBubble([
89091 /**
89092 * @event append
89093 * Fires when a new child node is appended.
89094 * @param {Ext.data.NodeInterface} this This node.
89095 * @param {Ext.data.NodeInterface} node The newly appended node.
89096 * @param {Number} index The index of the newly appended node.
89097 */
89098 "append",
89099
89100 /**
89101 * @event remove
89102 * Fires when a child node is removed.
89103 * @param {Ext.data.NodeInterface} this This node.
89104 * @param {Ext.data.NodeInterface} node The removed node.
89105 */
89106 "remove",
89107
89108 /**
89109 * @event move
89110 * Fires when this node is moved to a new location in the tree.
89111 * @param {Ext.data.NodeInterface} this This node.
89112 * @param {Ext.data.NodeInterface} oldParent The old parent of this node.
89113 * @param {Ext.data.NodeInterface} newParent The new parent of this node.
89114 * @param {Number} index The index it was moved to.
89115 */
89116 "move",
89117
89118 /**
89119 * @event insert
89120 * Fires when a new child node is inserted.
89121 * @param {Ext.data.NodeInterface} this This node.
89122 * @param {Ext.data.NodeInterface} node The child node inserted.
89123 * @param {Ext.data.NodeInterface} refNode The child node the node was inserted before.
89124 */
89125 "insert",
89126
89127 /**
89128 * @event beforeappend
89129 * Fires before a new child is appended, return `false` to cancel the append.
89130 * @param {Ext.data.NodeInterface} this This node.
89131 * @param {Ext.data.NodeInterface} node The child node to be appended.
89132 */
89133 "beforeappend",
89134
89135 /**
89136 * @event beforeremove
89137 * Fires before a child is removed, return `false` to cancel the remove.
89138 * @param {Ext.data.NodeInterface} this This node.
89139 * @param {Ext.data.NodeInterface} node The child node to be removed.
89140 */
89141 "beforeremove",
89142
89143 /**
89144 * @event beforemove
89145 * Fires before this node is moved to a new location in the tree. Return `false` to cancel the move.
89146 * @param {Ext.data.NodeInterface} this This node.
89147 * @param {Ext.data.NodeInterface} oldParent The parent of this node.
89148 * @param {Ext.data.NodeInterface} newParent The new parent this node is moving to.
89149 * @param {Number} index The index it is being moved to.
89150 */
89151 "beforemove",
89152
89153 /**
89154 * @event beforeinsert
89155 * Fires before a new child is inserted, return false to cancel the insert.
89156 * @param {Ext.data.NodeInterface} this This node
89157 * @param {Ext.data.NodeInterface} node The child node to be inserted
89158 * @param {Ext.data.NodeInterface} refNode The child node the node is being inserted before
89159 */
89160 "beforeinsert",
89161
89162 /**
89163 * @event expand
89164 * Fires when this node is expanded.
89165 * @param {Ext.data.NodeInterface} this The expanding node.
89166 */
89167 "expand",
89168
89169 /**
89170 * @event collapse
89171 * Fires when this node is collapsed.
89172 * @param {Ext.data.NodeInterface} this The collapsing node.
89173 */
89174 "collapse",
89175
89176 /**
89177 * @event beforeexpand
89178 * Fires before this node is expanded.
89179 * @param {Ext.data.NodeInterface} this The expanding node.
89180 */
89181 "beforeexpand",
89182
89183 /**
89184 * @event beforecollapse
89185 * Fires before this node is collapsed.
89186 * @param {Ext.data.NodeInterface} this The collapsing node.
89187 */
89188 "beforecollapse",
89189
89190 /**
89191 * @event sort
89192 * Fires when this node's childNodes are sorted.
89193 * @param {Ext.data.NodeInterface} this This node.
89194 * @param {Ext.data.NodeInterface[]} childNodes The childNodes of this node.
89195 */
89196 "sort",
89197
89198 'load'
89199 ]);
89200 }
89201
89202 return record;
89203 },
89204
89205 applyFields: function(modelClass, addFields) {
89206 var modelPrototype = modelClass.prototype,
89207 fields = modelPrototype.fields,
89208 keys = fields.keys,
89209 ln = addFields.length,
89210 addField, i,
89211 newFields = [];
89212
89213 for (i = 0; i < ln; i++) {
89214 addField = addFields[i];
89215 if (!Ext.Array.contains(keys, addField.name)) {
89216 addField = Ext.create('Ext.data.Field', addField);
89217
89218 newFields.push(addField);
89219 fields.add(addField);
89220 }
89221 }
89222
89223 return newFields;
89224 },
89225
89226 getPrototypeBody: function() {
89227 return {
89228 isNode: true,
89229
89230 /**
89231 * Ensures that the passed object is an instance of a Record with the NodeInterface applied
89232 * @return {Boolean}
89233 * @private
89234 */
89235 createNode: function(node) {
89236 if (Ext.isObject(node) && !node.isModel) {
89237 node = Ext.data.ModelManager.create(node, this.modelName);
89238 }
89239 // Make sure the node implements the node interface
89240 return Ext.data.NodeInterface.decorate(node);
89241 },
89242
89243 /**
89244 * Returns true if this node is a leaf
89245 * @return {Boolean}
89246 */
89247 isLeaf : function() {
89248 return this.get('leaf') === true;
89249 },
89250
89251 /**
89252 * Sets the first child of this node
89253 * @private
89254 * @param {Ext.data.NodeInterface} node
89255 */
89256 setFirstChild : function(node) {
89257 this.firstChild = node;
89258 },
89259
89260 /**
89261 * Sets the last child of this node
89262 * @private
89263 * @param {Ext.data.NodeInterface} node
89264 */
89265 setLastChild : function(node) {
89266 this.lastChild = node;
89267 },
89268
89269 /**
89270 * Updates general data of this node like isFirst, isLast, depth. This
89271 * method is internally called after a node is moved. This shouldn't
89272 * have to be called by the developer unless they are creating custom
89273 * Tree plugins.
89274 * @return {Boolean}
89275 */
89276 updateInfo: function(silent) {
89277 var me = this,
89278 parentNode = me.parentNode,
89279 isFirst = (!parentNode ? true : parentNode.firstChild == me),
89280 isLast = (!parentNode ? true : parentNode.lastChild == me),
89281 depth = 0,
89282 parent = me,
89283 children = me.childNodes,
89284 ln = children.length,
89285 i;
89286
89287 while (parent.parentNode) {
89288 ++depth;
89289 parent = parent.parentNode;
89290 }
89291
89292 me.beginEdit();
89293 me.set({
89294 isFirst: isFirst,
89295 isLast: isLast,
89296 depth: depth,
89297 index: parentNode ? parentNode.indexOf(me) : 0,
89298 parentId: parentNode ? parentNode.getId() : null
89299 });
89300 me.endEdit(silent);
89301 if (silent) {
89302 me.commit(silent);
89303 }
89304
89305 for (i = 0; i < ln; i++) {
89306 children[i].updateInfo(silent);
89307 }
89308 },
89309
89310 /**
89311 * Returns `true` if this node is the last child of its parent.
89312 * @return {Boolean}
89313 */
89314 isLast : function() {
89315 return this.get('isLast');
89316 },
89317
89318 /**
89319 * Returns `true` if this node is the first child of its parent.
89320 * @return {Boolean}
89321 */
89322 isFirst : function() {
89323 return this.get('isFirst');
89324 },
89325
89326 /**
89327 * Returns `true` if this node has one or more child nodes, else `false`.
89328 * @return {Boolean}
89329 */
89330 hasChildNodes : function() {
89331 return !this.isLeaf() && this.childNodes.length > 0;
89332 },
89333
89334 /**
89335 * Returns `true` if this node has one or more child nodes, or if the `expandable`
89336 * node attribute is explicitly specified as `true`, otherwise returns `false`.
89337 * @return {Boolean}
89338 */
89339 isExpandable : function() {
89340 var me = this;
89341
89342 if (me.get('expandable')) {
89343 return !(me.isLeaf() || (me.isLoaded() && !me.hasChildNodes()));
89344 }
89345 return false;
89346 },
89347
89348 /**
89349 * Insert node(s) as the last child node of this node.
89350 *
89351 * If the node was previously a child node of another parent node, it will be removed from that node first.
89352 *
89353 * @param {Ext.data.NodeInterface/Ext.data.NodeInterface[]} node The node or Array of nodes to append.
89354 * @return {Ext.data.NodeInterface} The appended node if single append, or `null` if an array was passed.
89355 */
89356 appendChild : function(node, suppressEvents, suppressNodeUpdate) {
89357 var me = this,
89358 i, ln,
89359 index,
89360 oldParent,
89361 ps;
89362
89363 // if passed an array or multiple args do them one by one
89364 if (Ext.isArray(node)) {
89365 for (i = 0, ln = node.length; i < ln; i++) {
89366 me.appendChild(node[i], suppressEvents, suppressNodeUpdate);
89367 }
89368 } else {
89369 // Make sure it is a record
89370 node = me.createNode(node);
89371
89372 if (suppressEvents !== true && me.fireEvent("beforeappend", me, node) === false) {
89373 return false;
89374 }
89375
89376 index = me.childNodes.length;
89377 oldParent = node.parentNode;
89378
89379 // it's a move, make sure we move it cleanly
89380 if (oldParent) {
89381 if (suppressEvents !== true && node.fireEvent("beforemove", node, oldParent, me, index) === false) {
89382 return false;
89383 }
89384 oldParent.removeChild(node, null, false, true);
89385 }
89386
89387 index = me.childNodes.length;
89388 if (index === 0) {
89389 me.setFirstChild(node);
89390 }
89391
89392 me.childNodes.push(node);
89393 node.parentNode = me;
89394 node.nextSibling = null;
89395
89396 me.setLastChild(node);
89397
89398 ps = me.childNodes[index - 1];
89399 if (ps) {
89400 node.previousSibling = ps;
89401 ps.nextSibling = node;
89402 ps.updateInfo(suppressNodeUpdate);
89403 } else {
89404 node.previousSibling = null;
89405 }
89406
89407 node.updateInfo(suppressNodeUpdate);
89408
89409 // As soon as we append a child to this node, we are loaded
89410 if (!me.isLoaded()) {
89411 me.set('loaded', true);
89412 }
89413 // If this node didn't have any childnodes before, update myself
89414 else if (me.childNodes.length === 1) {
89415 me.set('loaded', me.isLoaded());
89416 }
89417
89418 if (suppressEvents !== true) {
89419 me.fireEvent("append", me, node, index);
89420
89421 if (oldParent) {
89422 node.fireEvent("move", node, oldParent, me, index);
89423 }
89424 }
89425
89426 return node;
89427 }
89428 },
89429
89430 /**
89431 * Returns the bubble target for this node.
89432 * @private
89433 * @return {Object} The bubble target.
89434 */
89435 getBubbleTarget: function() {
89436 return this.parentNode;
89437 },
89438
89439 /**
89440 * Removes a child node from this node.
89441 * @param {Ext.data.NodeInterface} node The node to remove.
89442 * @param {Boolean} [destroy=false] `true` to destroy the node upon removal.
89443 * @return {Ext.data.NodeInterface} The removed node.
89444 */
89445 removeChild : function(node, destroy, suppressEvents, suppressNodeUpdate) {
89446 var me = this,
89447 index = me.indexOf(node);
89448
89449 if (index == -1 || (suppressEvents !== true && me.fireEvent("beforeremove", me, node) === false)) {
89450 return false;
89451 }
89452
89453 // remove it from childNodes collection
89454 Ext.Array.erase(me.childNodes, index, 1);
89455
89456 // update child refs
89457 if (me.firstChild == node) {
89458 me.setFirstChild(node.nextSibling);
89459 }
89460 if (me.lastChild == node) {
89461 me.setLastChild(node.previousSibling);
89462 }
89463
89464 if (suppressEvents !== true) {
89465 me.fireEvent("remove", me, node);
89466 }
89467
89468 // update siblings
89469 if (node.previousSibling) {
89470 node.previousSibling.nextSibling = node.nextSibling;
89471 node.previousSibling.updateInfo(suppressNodeUpdate);
89472 }
89473 if (node.nextSibling) {
89474 node.nextSibling.previousSibling = node.previousSibling;
89475 node.nextSibling.updateInfo(suppressNodeUpdate);
89476 }
89477
89478 // If this node suddenly doesn't have childnodes anymore, update myself
89479 if (!me.childNodes.length) {
89480 me.set('loaded', me.isLoaded());
89481 }
89482
89483 if (destroy) {
89484 node.destroy(true);
89485 } else {
89486 node.clear();
89487 }
89488
89489 return node;
89490 },
89491
89492 /**
89493 * Creates a copy (clone) of this Node.
89494 * @param {String} [newId] A new id, defaults to this Node's id.
89495 * @param {Boolean} [deep] If passed as `true`, all child Nodes are recursively copied into the new Node.
89496 * If omitted or `false`, the copy will have no child Nodes.
89497 * @return {Ext.data.NodeInterface} A copy of this Node.
89498 */
89499 copy: function(newId, deep) {
89500 var me = this,
89501 result = me.callOverridden(arguments),
89502 len = me.childNodes ? me.childNodes.length : 0,
89503 i;
89504
89505 // Move child nodes across to the copy if required
89506 if (deep) {
89507 for (i = 0; i < len; i++) {
89508 result.appendChild(me.childNodes[i].copy(true));
89509 }
89510 }
89511 return result;
89512 },
89513
89514 /**
89515 * Clear the node.
89516 * @private
89517 * @param {Boolean} destroy `true` to destroy the node.
89518 */
89519 clear : function(destroy) {
89520 var me = this;
89521
89522 // clear any references from the node
89523 me.parentNode = me.previousSibling = me.nextSibling = null;
89524 if (destroy) {
89525 me.firstChild = me.lastChild = null;
89526 }
89527 },
89528
89529 /**
89530 * Destroys the node.
89531 */
89532 destroy : function(silent) {
89533 /*
89534 * Silent is to be used in a number of cases
89535 * 1) When setRoot is called.
89536 * 2) When destroy on the tree is called
89537 * 3) For destroying child nodes on a node
89538 */
89539 var me = this,
89540 options = me.destroyOptions;
89541
89542 if (silent === true) {
89543 me.clear(true);
89544 Ext.each(me.childNodes, function(n) {
89545 n.destroy(true);
89546 });
89547 me.childNodes = null;
89548 delete me.destroyOptions;
89549 me.callOverridden([options]);
89550 } else {
89551 me.destroyOptions = silent;
89552 // overridden method will be called, since remove will end up calling destroy(true);
89553 me.remove(true);
89554 }
89555 },
89556
89557 /**
89558 * Inserts the first node before the second node in this nodes `childNodes` collection.
89559 * @param {Ext.data.NodeInterface} node The node to insert.
89560 * @param {Ext.data.NodeInterface} refNode The node to insert before (if `null` the node is appended).
89561 * @return {Ext.data.NodeInterface} The inserted node.
89562 */
89563 insertBefore : function(node, refNode, suppressEvents) {
89564 var me = this,
89565 index = me.indexOf(refNode),
89566 oldParent = node.parentNode,
89567 refIndex = index,
89568 ps;
89569
89570 if (!refNode) { // like standard Dom, refNode can be null for append
89571 return me.appendChild(node);
89572 }
89573
89574 // nothing to do
89575 if (node == refNode) {
89576 return false;
89577 }
89578
89579 // Make sure it is a record with the NodeInterface
89580 node = me.createNode(node);
89581
89582 if (suppressEvents !== true && me.fireEvent("beforeinsert", me, node, refNode) === false) {
89583 return false;
89584 }
89585
89586 // when moving internally, indexes will change after remove
89587 if (oldParent == me && me.indexOf(node) < index) {
89588 refIndex--;
89589 }
89590
89591 // it's a move, make sure we move it cleanly
89592 if (oldParent) {
89593 if (suppressEvents !== true && node.fireEvent("beforemove", node, oldParent, me, index, refNode) === false) {
89594 return false;
89595 }
89596 oldParent.removeChild(node);
89597 }
89598
89599 if (refIndex === 0) {
89600 me.setFirstChild(node);
89601 }
89602
89603 Ext.Array.splice(me.childNodes, refIndex, 0, node);
89604 node.parentNode = me;
89605
89606 node.nextSibling = refNode;
89607 refNode.previousSibling = node;
89608
89609 ps = me.childNodes[refIndex - 1];
89610 if (ps) {
89611 node.previousSibling = ps;
89612 ps.nextSibling = node;
89613 ps.updateInfo();
89614 } else {
89615 node.previousSibling = null;
89616 }
89617
89618 node.updateInfo();
89619
89620 if (!me.isLoaded()) {
89621 me.set('loaded', true);
89622 }
89623 // If this node didn't have any childnodes before, update myself
89624 else if (me.childNodes.length === 1) {
89625 me.set('loaded', me.isLoaded());
89626 }
89627
89628 if (suppressEvents !== true) {
89629 me.fireEvent("insert", me, node, refNode);
89630
89631 if (oldParent) {
89632 node.fireEvent("move", node, oldParent, me, refIndex, refNode);
89633 }
89634 }
89635
89636 return node;
89637 },
89638
89639 /**
89640 * Insert a node into this node.
89641 * @param {Number} index The zero-based index to insert the node at.
89642 * @param {Ext.data.Model} node The node to insert.
89643 * @return {Ext.data.Model} The record you just inserted.
89644 */
89645 insertChild: function(index, node) {
89646 var sibling = this.childNodes[index];
89647 if (sibling) {
89648 return this.insertBefore(node, sibling);
89649 }
89650 else {
89651 return this.appendChild(node);
89652 }
89653 },
89654
89655 /**
89656 * Removes this node from its parent.
89657 * @param {Boolean} [destroy=false] `true` to destroy the node upon removal.
89658 * @return {Ext.data.NodeInterface} this
89659 */
89660 remove : function(destroy, suppressEvents) {
89661 var parentNode = this.parentNode;
89662
89663 if (parentNode) {
89664 parentNode.removeChild(this, destroy, suppressEvents, true);
89665 }
89666 return this;
89667 },
89668
89669 /**
89670 * Removes all child nodes from this node.
89671 * @param {Boolean} [destroy=false] `true` to destroy the node upon removal.
89672 * @return {Ext.data.NodeInterface} this
89673 */
89674 removeAll : function(destroy, suppressEvents) {
89675 var cn = this.childNodes,
89676 n;
89677
89678 while ((n = cn[0])) {
89679 this.removeChild(n, destroy, suppressEvents);
89680 }
89681 return this;
89682 },
89683
89684 /**
89685 * Returns the child node at the specified index.
89686 * @param {Number} index
89687 * @return {Ext.data.NodeInterface}
89688 */
89689 getChildAt : function(index) {
89690 return this.childNodes[index];
89691 },
89692
89693 /**
89694 * Replaces one child node in this node with another.
89695 * @param {Ext.data.NodeInterface} newChild The replacement node.
89696 * @param {Ext.data.NodeInterface} oldChild The node to replace.
89697 * @return {Ext.data.NodeInterface} The replaced node.
89698 */
89699 replaceChild : function(newChild, oldChild, suppressEvents) {
89700 var s = oldChild ? oldChild.nextSibling : null;
89701
89702 this.removeChild(oldChild, suppressEvents);
89703 this.insertBefore(newChild, s, suppressEvents);
89704 return oldChild;
89705 },
89706
89707 /**
89708 * Returns the index of a child node.
89709 * @param {Ext.data.NodeInterface} child
89710 * @return {Number} The index of the node or -1 if it was not found.
89711 */
89712 indexOf : function(child) {
89713 return Ext.Array.indexOf(this.childNodes, child);
89714 },
89715
89716 /**
89717 * Gets the hierarchical path from the root of the current node.
89718 * @param {String} field (optional) The field to construct the path from. Defaults to the model `idProperty`.
89719 * @param {String} [separator=/] (optional) A separator to use.
89720 * @return {String} The node path
89721 */
89722 getPath: function(field, separator) {
89723 field = field || this.idProperty;
89724 separator = separator || '/';
89725
89726 var path = [this.get(field)],
89727 parent = this.parentNode;
89728
89729 while (parent) {
89730 path.unshift(parent.get(field));
89731 parent = parent.parentNode;
89732 }
89733 return separator + path.join(separator);
89734 },
89735
89736 /**
89737 * Returns depth of this node (the root node has a depth of 0).
89738 * @return {Number}
89739 */
89740 getDepth : function() {
89741 return this.get('depth');
89742 },
89743
89744 /**
89745 * Bubbles up the tree from this node, calling the specified function with each node. The arguments to the function
89746 * will be the args provided or the current node. If the function returns `false` at any point,
89747 * the bubble is stopped.
89748 * @param {Function} fn The function to call.
89749 * @param {Object} scope (optional) The scope (`this` reference) in which the function is executed. Defaults to the current Node.
89750 * @param {Array} args (optional) The args to call the function with (default to passing the current Node).
89751 */
89752 bubble : function(fn, scope, args) {
89753 var p = this;
89754 while (p) {
89755 if (fn.apply(scope || p, args || [p]) === false) {
89756 break;
89757 }
89758 p = p.parentNode;
89759 }
89760 },
89761
89762
89763 /**
89764 * Cascades down the tree from this node, calling the specified function with each node. The arguments to the function
89765 * will be the args provided or the current node. If the function returns false at any point,
89766 * the cascade is stopped on that branch.
89767 * @param {Function} fn The function to call
89768 * @param {Object} scope (optional) The scope (`this` reference) in which the function is executed. Defaults to the current Node.
89769 * @param {Array} args (optional) The args to call the function with (default to passing the current Node).
89770 */
89771 cascadeBy : function(fn, scope, args) {
89772 if (fn.apply(scope || this, args || [this]) !== false) {
89773 var childNodes = this.childNodes,
89774 length = childNodes.length,
89775 i;
89776
89777 for (i = 0; i < length; i++) {
89778 childNodes[i].cascadeBy(fn, scope, args);
89779 }
89780 }
89781 },
89782
89783 /**
89784 * Iterates the child nodes of this node, calling the specified function with each node. The arguments to the function
89785 * will be the args provided or the current node. If the function returns false at any point,
89786 * the iteration stops.
89787 * @param {Function} fn The function to call.
89788 * @param {Object} scope (optional) The scope (`this` reference) in which the function is executed. Defaults to the current Node in the iteration.
89789 * @param {Array} args (optional) The args to call the function with (default to passing the current Node).
89790 */
89791 eachChild : function(fn, scope, args) {
89792 var childNodes = this.childNodes,
89793 length = childNodes.length,
89794 i;
89795
89796 for (i = 0; i < length; i++) {
89797 if (fn.apply(scope || this, args || [childNodes[i]]) === false) {
89798 break;
89799 }
89800 }
89801 },
89802
89803 /**
89804 * Finds the first child that has the attribute with the specified value.
89805 * @param {String} attribute The attribute name.
89806 * @param {Object} value The value to search for.
89807 * @param {Boolean} deep (Optional) `true` to search through nodes deeper than the immediate children.
89808 * @return {Ext.data.NodeInterface} The found child or `null` if none was found.
89809 */
89810 findChild : function(attribute, value, deep) {
89811 return this.findChildBy(function() {
89812 return this.get(attribute) == value;
89813 }, null, deep);
89814 },
89815
89816 /**
89817 * Finds the first child by a custom function. The child matches if the function passed returns `true`.
89818 * @param {Function} fn A function which must return `true` if the passed Node is the required Node.
89819 * @param {Object} scope (optional) The scope (`this` reference) in which the function is executed. Defaults to the Node being tested.
89820 * @param {Boolean} deep (Optional) True to search through nodes deeper than the immediate children.
89821 * @return {Ext.data.NodeInterface} The found child or null if `none` was found.
89822 */
89823 findChildBy : function(fn, scope, deep) {
89824 var cs = this.childNodes,
89825 len = cs.length,
89826 i = 0, n, res;
89827
89828 for (; i < len; i++) {
89829 n = cs[i];
89830 if (fn.call(scope || n, n) === true) {
89831 return n;
89832 }
89833 else if (deep) {
89834 res = n.findChildBy(fn, scope, deep);
89835 if (res !== null) {
89836 return res;
89837 }
89838 }
89839 }
89840
89841 return null;
89842 },
89843
89844 /**
89845 * Returns `true` if this node is an ancestor (at any point) of the passed node.
89846 * @param {Ext.data.NodeInterface} node
89847 * @return {Boolean}
89848 */
89849 contains : function(node) {
89850 return node.isAncestor(this);
89851 },
89852
89853 /**
89854 * Returns `true` if the passed node is an ancestor (at any point) of this node.
89855 * @param {Ext.data.NodeInterface} node
89856 * @return {Boolean}
89857 */
89858 isAncestor : function(node) {
89859 var p = this.parentNode;
89860 while (p) {
89861 if (p == node) {
89862 return true;
89863 }
89864 p = p.parentNode;
89865 }
89866 return false;
89867 },
89868
89869 /**
89870 * Sorts this nodes children using the supplied sort function.
89871 * @param {Function} sortFn A function which, when passed two Nodes, returns -1, 0 or 1 depending upon required sort order.
89872 * @param {Boolean} recursive Whether or not to apply this sort recursively.
89873 * @param {Boolean} suppressEvent Set to true to not fire a sort event.
89874 */
89875 sort: function(sortFn, recursive, suppressEvent) {
89876 var cs = this.childNodes,
89877 ln = cs.length,
89878 i, n;
89879
89880 if (ln > 0) {
89881 Ext.Array.sort(cs, sortFn);
89882 for (i = 0; i < ln; i++) {
89883 n = cs[i];
89884 n.previousSibling = cs[i-1];
89885 n.nextSibling = cs[i+1];
89886
89887 if (i === 0) {
89888 this.setFirstChild(n);
89889 }
89890 if (i == ln - 1) {
89891 this.setLastChild(n);
89892 }
89893
89894 n.updateInfo(suppressEvent);
89895
89896 if (recursive && !n.isLeaf()) {
89897 n.sort(sortFn, true, true);
89898 }
89899 }
89900
89901 this.notifyStores('afterEdit', ['sorted'], {sorted: 'sorted'});
89902
89903 if (suppressEvent !== true) {
89904 this.fireEvent('sort', this, cs);
89905 }
89906 }
89907 },
89908
89909 /**
89910 * Returns `true` if this node is expanded.
89911 * @return {Boolean}
89912 */
89913 isExpanded: function() {
89914 return this.get('expanded');
89915 },
89916
89917 /**
89918 * Returns `true` if this node is loaded.
89919 * @return {Boolean}
89920 */
89921 isLoaded: function() {
89922 return this.get('loaded');
89923 },
89924
89925 /**
89926 * Returns `true` if this node is loading.
89927 * @return {Boolean}
89928 */
89929 isLoading: function() {
89930 return this.get('loading');
89931 },
89932
89933 /**
89934 * Returns `true` if this node is the root node.
89935 * @return {Boolean}
89936 */
89937 isRoot: function() {
89938 return !this.parentNode;
89939 },
89940
89941 /**
89942 * Returns `true` if this node is visible.
89943 * @return {Boolean}
89944 */
89945 isVisible: function() {
89946 var parent = this.parentNode;
89947 while (parent) {
89948 if (!parent.isExpanded()) {
89949 return false;
89950 }
89951 parent = parent.parentNode;
89952 }
89953 return true;
89954 },
89955
89956 /**
89957 * Expand this node.
89958 * @param {Function} recursive (Optional) `true` to recursively expand all the children.
89959 * @param {Function} callback (Optional) The function to execute once the expand completes.
89960 * @param {Object} scope (Optional) The scope to run the callback in.
89961 */
89962 expand: function(recursive, callback, scope) {
89963 var me = this;
89964
89965 if (!me.isLeaf()) {
89966 if (me.isLoading()) {
89967 me.on('expand', function() {
89968 me.expand(recursive, callback, scope);
89969 }, me, {single: true});
89970 }
89971 else {
89972 if (!me.isExpanded()) {
89973 // The TreeStore actually listens for the beforeexpand method and checks
89974 // whether we have to asynchronously load the children from the server
89975 // first. Thats why we pass a callback function to the event that the
89976 // store can call once it has loaded and parsed all the children.
89977 me.fireAction('expand', [this], function() {
89978 me.set('expanded', true);
89979 Ext.callback(callback, scope || me, [me.childNodes]);
89980 });
89981 }
89982 else {
89983 Ext.callback(callback, scope || me, [me.childNodes]);
89984 }
89985 }
89986 } else {
89987 Ext.callback(callback, scope || me);
89988 }
89989 },
89990
89991 /**
89992 * Collapse this node.
89993 * @param {Function} recursive (Optional) `true` to recursively collapse all the children.
89994 * @param {Function} callback (Optional) The function to execute once the collapse completes.
89995 * @param {Object} scope (Optional) The scope to run the callback in.
89996 */
89997 collapse: function(recursive, callback, scope) {
89998 var me = this;
89999
90000 // First we start by checking if this node is a parent
90001 if (!me.isLeaf() && me.isExpanded()) {
90002 this.fireAction('collapse', [me], function() {
90003 me.set('expanded', false);
90004 Ext.callback(callback, scope || me, [me.childNodes]);
90005 });
90006 } else {
90007 Ext.callback(callback, scope || me, [me.childNodes]);
90008 }
90009 }
90010 };
90011 }
90012 }
90013 });
90014
90015 /**
90016 * @private
90017 */
90018 Ext.define('Ext.data.NodeStore', {
90019 extend: Ext.data.Store ,
90020 alias: 'store.node',
90021
90022
90023 config: {
90024 /**
90025 * @cfg {Ext.data.Model} node The Record you want to bind this Store to. Note that
90026 * this record will be decorated with the {@link Ext.data.NodeInterface} if this is not the
90027 * case yet.
90028 * @accessor
90029 */
90030 node: null,
90031
90032 /**
90033 * @cfg {Boolean} recursive Set this to `true` if you want this NodeStore to represent
90034 * all the descendants of the node in its flat data collection. This is useful for
90035 * rendering a tree structure to a DataView and is being used internally by
90036 * the TreeView. Any records that are moved, removed, inserted or appended to the
90037 * node at any depth below the node this store is bound to will be automatically
90038 * updated in this Store's internal flat data structure.
90039 * @accessor
90040 */
90041 recursive: false,
90042
90043 /**
90044 * @cfg {Boolean} rootVisible `false` to not include the root node in this Stores collection.
90045 * @accessor
90046 */
90047 rootVisible: false,
90048
90049 sorters: undefined,
90050 filters: undefined,
90051
90052 /**
90053 * @cfg {Boolean} folderSort
90054 * Set to `true` to automatically prepend a leaf sorter.
90055 */
90056 folderSort: false
90057 },
90058
90059 afterEdit: function(record, modifiedFields) {
90060 if (modifiedFields) {
90061 if (modifiedFields.indexOf('loaded') !== -1) {
90062 return this.add(this.retrieveChildNodes(record));
90063 }
90064 if (modifiedFields.indexOf('expanded') !== -1) {
90065 return this.filter();
90066 }
90067 if (modifiedFields.indexOf('sorted') !== -1) {
90068 return this.sort();
90069 }
90070 }
90071 this.callParent(arguments);
90072 },
90073
90074 onNodeAppend: function(parent, node) {
90075 this.add([node].concat(this.retrieveChildNodes(node)));
90076 },
90077
90078 onNodeInsert: function(parent, node) {
90079 this.add([node].concat(this.retrieveChildNodes(node)));
90080 },
90081
90082 onNodeRemove: function(parent, node) {
90083 this.remove([node].concat(this.retrieveChildNodes(node)));
90084 },
90085
90086 onNodeSort: function() {
90087 this.sort();
90088 },
90089
90090 updateFolderSort: function(folderSort) {
90091 if (folderSort) {
90092 this.setGrouper(function(node) {
90093 if (node.isLeaf()) {
90094 return 1;
90095 }
90096 return 0;
90097 });
90098 } else {
90099 this.setGrouper(null);
90100 }
90101 },
90102
90103 createDataCollection: function() {
90104 var collection = this.callParent();
90105 collection.handleSort = Ext.Function.bind(this.handleTreeSort, this, [collection], true);
90106 collection.findInsertionIndex = Ext.Function.bind(this.handleTreeInsertionIndex, this, [collection, collection.findInsertionIndex], true);
90107 return collection;
90108 },
90109
90110 handleTreeInsertionIndex: function(items, item, collection, originalFn) {
90111 return originalFn.call(collection, items, item, this.treeSortFn);
90112 },
90113
90114 handleTreeSort: function(data) {
90115 Ext.Array.sort(data, this.treeSortFn);
90116 return data;
90117 },
90118
90119 /**
90120 * This is a custom tree sorting algorithm. It uses the index property on each node to determine
90121 * how to sort siblings. It uses the depth property plus the index to create a weight for each node.
90122 * This weight algorithm has the limitation of not being able to go more then 80 levels in depth, or
90123 * more then 10k nodes per parent. The end result is a flat collection being correctly sorted based
90124 * on this one single sort function.
90125 * @param {Ext.data.NodeInterface} node1
90126 * @param {Ext.data.NodeInterface} node2
90127 * @return {Number}
90128 * @private
90129 */
90130 treeSortFn: function(node1, node2) {
90131 // A shortcut for siblings
90132 if (node1.parentNode === node2.parentNode) {
90133 return (node1.data.index < node2.data.index) ? -1 : 1;
90134 }
90135
90136 // @NOTE: with the following algorithm we can only go 80 levels deep in the tree
90137 // and each node can contain 10000 direct children max
90138 var weight1 = 0,
90139 weight2 = 0,
90140 parent1 = node1,
90141 parent2 = node2;
90142
90143 while (parent1) {
90144 weight1 += (Math.pow(10, (parent1.data.depth+1) * -4) * (parent1.data.index+1));
90145 parent1 = parent1.parentNode;
90146 }
90147 while (parent2) {
90148 weight2 += (Math.pow(10, (parent2.data.depth+1) * -4) * (parent2.data.index+1));
90149 parent2 = parent2.parentNode;
90150 }
90151
90152 if (weight1 > weight2) {
90153 return 1;
90154 } else if (weight1 < weight2) {
90155 return -1;
90156 }
90157 return (node1.data.index > node2.data.index) ? 1 : -1;
90158 },
90159
90160 applyFilters: function(filters) {
90161 var me = this;
90162 return function(item) {
90163 return me.isVisible(item);
90164 };
90165 },
90166
90167 applyProxy: function(proxy) {
90168 //<debug>
90169 if (proxy) {
90170 Ext.Logger.warn("A NodeStore cannot be bound to a proxy. Instead bind it to a record " +
90171 "decorated with the NodeInterface by setting the node config.");
90172 }
90173 //</debug>
90174 },
90175
90176 applyNode: function(node) {
90177 if (node) {
90178 node = Ext.data.NodeInterface.decorate(node);
90179 }
90180 return node;
90181 },
90182
90183 updateNode: function(node, oldNode) {
90184 if (oldNode && !oldNode.isDestroyed) {
90185 oldNode.un({
90186 append : 'onNodeAppend',
90187 insert : 'onNodeInsert',
90188 remove : 'onNodeRemove',
90189 load : 'onNodeLoad',
90190 scope: this
90191 });
90192 oldNode.unjoin(this);
90193 }
90194
90195 if (node) {
90196 node.on({
90197 scope : this,
90198 append : 'onNodeAppend',
90199 insert : 'onNodeInsert',
90200 remove : 'onNodeRemove',
90201 load : 'onNodeLoad'
90202 });
90203
90204 node.join(this);
90205
90206 var data = [];
90207 if (node.childNodes.length) {
90208 data = data.concat(this.retrieveChildNodes(node));
90209 }
90210 if (this.getRootVisible()) {
90211 data.push(node);
90212 } else if (node.isLoaded() || node.isLoading()) {
90213 node.set('expanded', true);
90214 }
90215
90216 this.data.clear();
90217 this.fireEvent('clear', this);
90218
90219 this.suspendEvents();
90220 this.add(data);
90221 this.resumeEvents();
90222
90223 if(data.length === 0) {
90224 this.loaded = node.loaded = true;
90225 }
90226
90227 this.fireEvent('refresh', this, this.data);
90228 }
90229 },
90230
90231 /**
90232 * Private method used to deeply retrieve the children of a record without recursion.
90233 * @private
90234 * @param {Ext.data.NodeInterface} root
90235 * @return {Array}
90236 */
90237 retrieveChildNodes: function(root) {
90238 var node = this.getNode(),
90239 recursive = this.getRecursive(),
90240 added = [],
90241 child = root;
90242
90243 if (!root.childNodes.length || (!recursive && root !== node)) {
90244 return added;
90245 }
90246
90247 if (!recursive) {
90248 return root.childNodes;
90249 }
90250
90251 while (child) {
90252 if (child._added) {
90253 delete child._added;
90254 if (child === root) {
90255 break;
90256 } else {
90257 child = child.nextSibling || child.parentNode;
90258 }
90259 } else {
90260 if (child !== root) {
90261 added.push(child);
90262 }
90263 if (child.firstChild) {
90264 child._added = true;
90265 child = child.firstChild;
90266 } else {
90267 child = child.nextSibling || child.parentNode;
90268 }
90269 }
90270 }
90271
90272 return added;
90273 },
90274
90275 /**
90276 * @param {Object} node
90277 * @return {Boolean}
90278 */
90279 isVisible: function(node) {
90280 var parent = node.parentNode;
90281
90282 if (!this.getRecursive() && parent !== this.getNode()) {
90283 return false;
90284 }
90285
90286 while (parent) {
90287 if (!parent.isExpanded()) {
90288 return false;
90289 }
90290
90291 //we need to check this because for a nodestore the node is not likely to be the root
90292 //so we stop going up the chain when we hit the original node as we don't care about any
90293 //ancestors above the configured node
90294 if (parent === this.getNode()) {
90295 break;
90296 }
90297
90298 parent = parent.parentNode;
90299 }
90300 return true;
90301 }
90302 });
90303
90304 /**
90305 * The TreeStore is a store implementation that allows for nested data.
90306 *
90307 * It provides convenience methods for loading nodes, as well as the ability to use
90308 * the hierarchical tree structure combined with a store. This class also relays many events from
90309 * the Tree for convenience.
90310 *
90311 * # Using Models
90312 *
90313 * If no Model is specified, an implicit model will be created that implements {@link Ext.data.NodeInterface}.
90314 * The standard Tree fields will also be copied onto the Model for maintaining their state. These fields are listed
90315 * in the {@link Ext.data.NodeInterface} documentation.
90316 *
90317 * # Reading Nested Data
90318 *
90319 * For the tree to read nested data, the {@link Ext.data.reader.Reader} must be configured with a root property,
90320 * so the reader can find nested data for each node. If a root is not specified, it will default to
90321 * 'children'.
90322 *
90323 * ###Further Reading
90324 * [Sencha Touch Data Overview](../../../core_concepts/data/data_package_overview.html)
90325 * [Sencha Touch Store Guide](../../../core_concepts/data/stores.html)
90326 * [Sencha Touch Models Guide](../../../core_concepts/data/models.html)
90327 * [Sencha Touch Proxy Guide](../../../core_concepts/data/proxies.html)
90328 */
90329 Ext.define('Ext.data.TreeStore', {
90330 extend: Ext.data.NodeStore ,
90331 alias: 'store.tree',
90332
90333 config: {
90334 /**
90335 * @cfg {Ext.data.Model/Ext.data.NodeInterface/Object} root
90336 * The root node for this store. For example:
90337 *
90338 * root: {
90339 * expanded: true,
90340 * text: "My Root",
90341 * children: [
90342 * { text: "Child 1", leaf: true },
90343 * { text: "Child 2", expanded: true, children: [
90344 * { text: "GrandChild", leaf: true }
90345 * ] }
90346 * ]
90347 * }
90348 *
90349 * Setting the `root` config option is the same as calling {@link #setRootNode}.
90350 * @accessor
90351 */
90352 root: undefined,
90353
90354 /**
90355 * @cfg {Boolean} clearOnLoad
90356 * Remove previously existing child nodes before loading. Default to true.
90357 * @accessor
90358 */
90359 clearOnLoad : true,
90360
90361 /**
90362 * @cfg {String} nodeParam
90363 * The name of the parameter sent to the server which contains the identifier of the node.
90364 * Defaults to 'node'.
90365 * @accessor
90366 */
90367 nodeParam: 'node',
90368
90369 /**
90370 * @cfg {String} defaultRootId
90371 * The default root id. Defaults to 'root'
90372 * @accessor
90373 */
90374 defaultRootId: 'root',
90375
90376 /**
90377 * @cfg {String} defaultRootProperty
90378 * The root property to specify on the reader if one is not explicitly defined.
90379 * @accessor
90380 */
90381 defaultRootProperty: 'children',
90382
90383 /**
90384 * @cfg {Boolean} recursive
90385 * @private
90386 * @hide
90387 */
90388 recursive: true
90389
90390 /**
90391 * @cfg {Object} node
90392 * @private
90393 * @hide
90394 */
90395 },
90396
90397 applyProxy: function() {
90398 return Ext.data.Store.prototype.applyProxy.apply(this, arguments);
90399 },
90400
90401 applyRoot: function(root) {
90402 var me = this;
90403 root = root || {};
90404 root = Ext.apply({}, root);
90405
90406 if (!root.isModel) {
90407 Ext.applyIf(root, {
90408 id: me.getStoreId() + '-' + me.getDefaultRootId(),
90409 text: 'Root',
90410 allowDrag: false
90411 });
90412
90413 root = Ext.data.ModelManager.create(root, me.getModel());
90414 }
90415
90416 Ext.data.NodeInterface.decorate(root);
90417 root.set(root.raw);
90418
90419 return root;
90420 },
90421
90422 handleTreeInsertionIndex: function(items, item, collection, originalFn) {
90423 if (item.parentNode) {
90424 item.parentNode.sort(collection.getSortFn(), true, true);
90425 }
90426 return this.callParent(arguments);
90427 },
90428
90429 handleTreeSort: function(data, collection) {
90430 if (this._sorting) {
90431 return data;
90432 }
90433
90434 this._sorting = true;
90435 this.getNode().sort(collection.getSortFn(), true, true);
90436 delete this._sorting;
90437 return this.callParent(arguments);
90438 },
90439
90440 updateRoot: function(root, oldRoot) {
90441 if (oldRoot) {
90442 oldRoot.unBefore({
90443 expand: 'onNodeBeforeExpand',
90444 scope: this
90445 });
90446 oldRoot.unjoin(this);
90447 }
90448
90449 root.onBefore({
90450 expand: 'onNodeBeforeExpand',
90451 scope: this
90452 });
90453
90454 this.onNodeAppend(null, root);
90455 this.setNode(root);
90456
90457 if (!root.isLoaded() && !root.isLoading() && root.isExpanded()) {
90458 this.load({
90459 node: root
90460 });
90461 }
90462
90463 /**
90464 * @event rootchange
90465 * Fires whenever the root node changes on this TreeStore.
90466 * @param {Ext.data.TreeStore} store This tree Store
90467 * @param {Ext.data.Model} newRoot The new root node
90468 * @param {Ext.data.Model} oldRoot The old root node
90469 */
90470 this.fireEvent('rootchange', this, root, oldRoot);
90471 },
90472
90473 /**
90474 * Returns the record node by id
90475 * @return {Ext.data.NodeInterface}
90476 */
90477 getNodeById: function(id) {
90478 return this.data.getByKey(id);
90479 },
90480
90481 getById: function(id) {
90482 return this.data.getByKey(id);
90483 },
90484
90485 onNodeBeforeExpand: function(node, options, e) {
90486 if (node.isLoading()) {
90487 e.pause();
90488 this.on('load', function() {
90489 e.resume();
90490 }, this, {single: true});
90491 }
90492 else if (!node.isLoaded()) {
90493 e.pause();
90494 this.load({
90495 node: node,
90496 callback: function() {
90497 e.resume();
90498 }
90499 });
90500 }
90501 },
90502
90503 onNodeAppend: function(parent, node) {
90504 var proxy = this.getProxy(),
90505 reader = proxy.getReader(),
90506 Model = this.getModel(),
90507 data = node.raw,
90508 records = [],
90509 rootProperty = reader.getRootProperty(),
90510 dataRoot, processedData, i, ln, processedDataItem;
90511
90512 if (!node.isLeaf()) {
90513 dataRoot = reader.getRoot(data);
90514 if (dataRoot) {
90515 processedData = reader.extractData(dataRoot);
90516 for (i = 0, ln = processedData.length; i < ln; i++) {
90517 processedDataItem = processedData[i];
90518 records.push(new Model(processedDataItem.data, processedDataItem.id, processedDataItem.node));
90519 }
90520
90521 if (records.length) {
90522 this.fillNode(node, records);
90523 } else {
90524 node.set('loaded', true);
90525 }
90526 // If the child record is not a leaf, and it has a data root (e.g. items: [])
90527 // and there are items in this data root, then we call fillNode to automatically
90528 // add these items. fillNode sets the loaded property on the node, meaning that
90529 // the next time you expand that node, it's not going to the server to request the
90530 // children. If however you pass back an empty array as items, we have to set the
90531 // loaded property to true here as well to prevent the items from being be loaded
90532 // from the server the next time you expand it.
90533 // If you want to have the items loaded on the next expand, then the data for the
90534 // node should not contain the items: [] array.
90535 delete data[rootProperty];
90536 }
90537 }
90538 },
90539
90540 updateAutoLoad: function(autoLoad) {
90541 if (autoLoad) {
90542 var root = this.getRoot();
90543 if (!root.isLoaded() && !root.isLoading()) {
90544 this.load({node: root});
90545 }
90546 }
90547 },
90548
90549 /**
90550 * Loads the Store using its configured {@link #proxy}.
90551 * @param {Object} options (Optional) config object. This is passed into the {@link Ext.data.Operation Operation}
90552 * object that is created and then sent to the proxy's {@link Ext.data.proxy.Proxy#read} function.
90553 * The options can also contain a node, which indicates which node is to be loaded. If not specified, it will
90554 * default to the root node.
90555 * @return {Object}
90556 */
90557 load: function(options) {
90558 options = options || {};
90559 options.params = options.params || {};
90560
90561 var me = this,
90562 node = options.node = options.node || me.getRoot();
90563
90564 options.params[me.getNodeParam()] = node.getId();
90565
90566 if (me.getClearOnLoad()) {
90567 node.removeAll(true);
90568 }
90569 node.set('loading', true);
90570
90571 return me.callParent([options]);
90572 },
90573
90574 updateProxy: function(proxy) {
90575 this.callParent(arguments);
90576
90577 var reader = proxy.getReader();
90578 if (!reader.getRootProperty()) {
90579 reader.setRootProperty(this.getDefaultRootProperty());
90580 reader.buildExtractors();
90581 }
90582 },
90583
90584 /**
90585 * @inheritdoc
90586 */
90587 removeAll: function() {
90588 this.getRoot().removeAll(true);
90589 this.callParent(arguments);
90590 },
90591
90592 /**
90593 * @inheritdoc
90594 */
90595 onProxyLoad: function(operation) {
90596 var me = this,
90597 records = operation.getRecords(),
90598 successful = operation.wasSuccessful(),
90599 node = operation.getNode();
90600
90601 node.beginEdit();
90602 node.set('loading', false);
90603 if (successful) {
90604 records = me.fillNode(node, records);
90605 }
90606 node.endEdit();
90607
90608 me.loading = false;
90609 me.loaded = true;
90610
90611 node.fireEvent('load', node, records, successful);
90612 me.fireEvent('load', this, records, successful, operation);
90613
90614 //this is a callback that would have been passed to the 'read' function and is optional
90615 Ext.callback(operation.getCallback(), operation.getScope() || me, [records, operation, successful]);
90616 },
90617
90618 /**
90619 * Fills a node with a series of child records.
90620 * @private
90621 * @param {Ext.data.NodeInterface} node The node to fill.
90622 * @param {Ext.data.Model[]} records The records to add.
90623 */
90624 fillNode: function(node, records) {
90625 var ln = records ? records.length : 0,
90626 i, child;
90627
90628 for (i = 0; i < ln; i++) {
90629 // true/true to suppress any events fired by the node, or the new child node
90630 child = node.appendChild(records[i], true, true);
90631 this.onNodeAppend(node, child);
90632 }
90633 node.set('loaded', true);
90634
90635 return records;
90636 }
90637
90638 });
90639
90640 /**
90641 * @author Tommy Maintz
90642 *
90643 * This class is a sequential id generator. A simple use of this class would be like so:
90644 *
90645 * Ext.define('MyApp.data.MyModel', {
90646 * extend: 'Ext.data.Model',
90647 * config: {
90648 * identifier: 'sequential'
90649 * }
90650 * });
90651 * // assign id's of 1, 2, 3, etc.
90652 *
90653 * An example of a configured generator would be:
90654 *
90655 * Ext.define('MyApp.data.MyModel', {
90656 * extend: 'Ext.data.Model',
90657 * config: {
90658 * identifier: {
90659 * type: 'sequential',
90660 * prefix: 'ID_',
90661 * seed: 1000
90662 * }
90663 * }
90664 * });
90665 * // assign id's of ID_1000, ID_1001, ID_1002, etc.
90666 *
90667 */
90668 Ext.define('Ext.data.identifier.Sequential', {
90669 extend: Ext.data.identifier.Simple ,
90670 alias: 'data.identifier.sequential',
90671
90672 config: {
90673 /**
90674 * @cfg {String} prefix
90675 * The string to place in front of the sequential number for each generated id. The
90676 * default is blank.
90677 */
90678 prefix: '',
90679
90680 /**
90681 * @cfg {Number} seed
90682 * The number at which to start generating sequential id's. The default is 1.
90683 */
90684 seed: 1
90685 },
90686
90687 constructor: function() {
90688 var me = this;
90689 me.callParent(arguments);
90690 me.parts = [me.getPrefix(), ''];
90691 },
90692
90693 generate: function(record) {
90694 var me = this,
90695 parts = me.parts,
90696 seed = me.getSeed() + 1;
90697
90698 me.setSeed(seed);
90699 parts[1] = seed;
90700
90701 return parts.join('');
90702 }
90703 });
90704
90705 /**
90706 * @author Tommy Maintz
90707 *
90708 * This class generates UUID's according to RFC 4122. This class has a default id property.
90709 * This means that a single instance is shared unless the id property is overridden. Thus,
90710 * two {@link Ext.data.Model} instances configured like the following share one generator:
90711 *
90712 * Ext.define('MyApp.data.MyModelX', {
90713 * extend: 'Ext.data.Model',
90714 * config: {
90715 * identifier: 'uuid'
90716 * }
90717 * });
90718 *
90719 * Ext.define('MyApp.data.MyModelY', {
90720 * extend: 'Ext.data.Model',
90721 * config: {
90722 * identifier: 'uuid'
90723 * }
90724 * });
90725 *
90726 * This allows all models using this class to share a commonly configured instance.
90727 *
90728 * # Using Version 1 ("Sequential") UUID's
90729 *
90730 * If a server can provide a proper timestamp and a "cryptographic quality random number"
90731 * (as described in RFC 4122), the shared instance can be configured as follows:
90732 *
90733 * Ext.data.identifier.Uuid.Global.reconfigure({
90734 * version: 1,
90735 * clockSeq: clock, // 14 random bits
90736 * salt: salt, // 48 secure random bits (the Node field)
90737 * timestamp: ts // timestamp per Section 4.1.4
90738 * });
90739 *
90740 * // or these values can be split into 32-bit chunks:
90741 *
90742 * Ext.data.identifier.Uuid.Global.reconfigure({
90743 * version: 1,
90744 * clockSeq: clock,
90745 * salt: { lo: saltLow32, hi: saltHigh32 },
90746 * timestamp: { lo: timestampLow32, hi: timestamptHigh32 }
90747 * });
90748 *
90749 * This approach improves the generator's uniqueness by providing a valid timestamp and
90750 * higher quality random data. Version 1 UUID's should not be used unless this information
90751 * can be provided by a server and care should be taken to avoid caching of this data.
90752 *
90753 * See [http://www.ietf.org/rfc/rfc4122.txt](http://www.ietf.org/rfc/rfc4122.txt) for details.
90754 */
90755 Ext.define('Ext.data.identifier.Uuid', {
90756 extend: Ext.data.identifier.Simple ,
90757 alias: 'data.identifier.uuid',
90758
90759 /**
90760 * Provides a way to determine if this identifier supports creating unique IDs. Proxies like {@link Ext.data.proxy.LocalStorage}
90761 * need the identifier to create unique IDs and will check this property.
90762 * @property isUnique
90763 * @type Boolean
90764 * @private
90765 */
90766 isUnique: true,
90767
90768 config: {
90769 /**
90770 * The id for this generator instance. By default all model instances share the same
90771 * UUID generator instance. By specifying an id other then 'uuid', a unique generator instance
90772 * will be created for the Model.
90773 */
90774 id: undefined,
90775
90776 /**
90777 * @property {Number/Object} salt
90778 * When created, this value is a 48-bit number. For computation, this value is split
90779 * into 32-bit parts and stored in an object with `hi` and `lo` properties.
90780 */
90781 salt: null,
90782
90783 /**
90784 * @property {Number/Object} timestamp
90785 * When created, this value is a 60-bit number. For computation, this value is split
90786 * into 32-bit parts and stored in an object with `hi` and `lo` properties.
90787 */
90788 timestamp: null,
90789
90790 /**
90791 * @cfg {Number} version
90792 * The Version of UUID. Supported values are:
90793 *
90794 * * 1 : Time-based, "sequential" UUID.
90795 * * 4 : Pseudo-random UUID.
90796 *
90797 * The default is 4.
90798 */
90799 version: 4
90800 },
90801
90802 applyId: function(id) {
90803 if (id === undefined) {
90804 return Ext.data.identifier.Uuid.Global;
90805 }
90806 return id;
90807 },
90808
90809 constructor: function() {
90810 var me = this;
90811 me.callParent(arguments);
90812 me.parts = [];
90813 me.init();
90814 },
90815
90816 /**
90817 * Reconfigures this generator given new config properties.
90818 */
90819 reconfigure: function(config) {
90820 this.setConfig(config);
90821 this.init();
90822 },
90823
90824 generate: function () {
90825 var me = this,
90826 parts = me.parts,
90827 version = me.getVersion(),
90828 salt = me.getSalt(),
90829 time = me.getTimestamp();
90830
90831 /*
90832 The magic decoder ring (derived from RFC 4122 Section 4.2.2):
90833
90834 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
90835 | time_low |
90836 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
90837 | time_mid | ver | time_hi |
90838 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
90839 |res| clock_hi | clock_low | salt 0 |M| salt 1 |
90840 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
90841 | salt (2-5) |
90842 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
90843
90844 time_mid clock_hi (low 6 bits)
90845 time_low | time_hi |clock_lo
90846 | | | || salt[0]
90847 | | | || | salt[1..5]
90848 v v v vv v v
90849 0badf00d-aced-1def-b123-dfad0badbeef
90850 ^ ^ ^
90851 version | multicast (low bit)
90852 |
90853 reserved (upper 2 bits)
90854 */
90855 parts[0] = me.toHex(time.lo, 8);
90856 parts[1] = me.toHex(time.hi & 0xFFFF, 4);
90857 parts[2] = me.toHex(((time.hi >>> 16) & 0xFFF) | (version << 12), 4);
90858 parts[3] = me.toHex(0x80 | ((me.clockSeq >>> 8) & 0x3F), 2) +
90859 me.toHex(me.clockSeq & 0xFF, 2);
90860 parts[4] = me.toHex(salt.hi, 4) + me.toHex(salt.lo, 8);
90861
90862 if (version == 4) {
90863 me.init(); // just regenerate all the random values...
90864 } else {
90865 // sequentially increment the timestamp...
90866 ++time.lo;
90867 if (time.lo >= me.twoPow32) { // if (overflow)
90868 time.lo = 0;
90869 ++time.hi;
90870 }
90871 }
90872
90873 return parts.join('-').toLowerCase();
90874 },
90875
90876 /**
90877 * @private
90878 */
90879 init: function () {
90880 var me = this,
90881 salt = me.getSalt(),
90882 time = me.getTimestamp();
90883
90884 if (me.getVersion() == 4) {
90885 // See RFC 4122 (Secion 4.4)
90886 // o If the state was unavailable (e.g., non-existent or corrupted),
90887 // or the saved node ID is different than the current node ID,
90888 // generate a random clock sequence value.
90889 me.clockSeq = me.rand(0, me.twoPow14-1);
90890
90891 if (!salt) {
90892 salt = {};
90893 me.setSalt(salt);
90894 }
90895
90896 if (!time) {
90897 time = {};
90898 me.setTimestamp(time);
90899 }
90900
90901 // See RFC 4122 (Secion 4.4)
90902 salt.lo = me.rand(0, me.twoPow32-1);
90903 salt.hi = me.rand(0, me.twoPow16-1);
90904 time.lo = me.rand(0, me.twoPow32-1);
90905 time.hi = me.rand(0, me.twoPow28-1);
90906 } else {
90907 // this is run only once per-instance
90908 me.setSalt(me.split(me.getSalt()));
90909 me.setTimestamp(me.split(me.getTimestamp()));
90910
90911 // Set multicast bit: "the least significant bit of the first octet of the
90912 // node ID" (nodeId = salt for this implementation):
90913 me.getSalt().hi |= 0x100;
90914 }
90915 },
90916
90917 /**
90918 * Some private values used in methods on this class.
90919 * @private
90920 */
90921 twoPow14: Math.pow(2, 14),
90922 twoPow16: Math.pow(2, 16),
90923 twoPow28: Math.pow(2, 28),
90924 twoPow32: Math.pow(2, 32),
90925
90926 /**
90927 * Converts a value into a hexadecimal value. Also allows for a maximum length
90928 * of the returned value.
90929 * @param {String} value
90930 * @param {Number} length
90931 * @private
90932 */
90933 toHex: function(value, length) {
90934 var ret = value.toString(16);
90935 if (ret.length > length) {
90936 ret = ret.substring(ret.length - length); // right-most digits
90937 } else if (ret.length < length) {
90938 ret = Ext.String.leftPad(ret, length, '0');
90939 }
90940 return ret;
90941 },
90942
90943 /**
90944 * Generates a random value with between a low and high.
90945 * @param {Number} low
90946 * @param {Number} high
90947 * @private
90948 */
90949 rand: function(low, high) {
90950 var v = Math.random() * (high - low + 1);
90951 return Math.floor(v) + low;
90952 },
90953
90954 /**
90955 * Splits a number into a low and high value.
90956 * @param {Number} bignum
90957 * @private
90958 */
90959 split: function(bignum) {
90960 if (typeof(bignum) == 'number') {
90961 var hi = Math.floor(bignum / this.twoPow32);
90962 return {
90963 lo: Math.floor(bignum - hi * this.twoPow32),
90964 hi: hi
90965 };
90966 }
90967 return bignum;
90968 }
90969 }, function() {
90970 this.Global = new this({
90971 id: 'uuid'
90972 });
90973 });
90974
90975 /**
90976 * @author Ed Spencer
90977 *
90978 * The JsonP proxy is useful when you need to load data from a domain other than the one your application is running on. If
90979 * your application is running on http://domainA.com it cannot use {@link Ext.data.proxy.Ajax Ajax} to load its data
90980 * from http://domainB.com because cross-domain ajax requests are prohibited by the browser.
90981 *
90982 * We can get around this using a JsonP proxy. JsonP proxy injects a `<script>` tag into the DOM whenever an AJAX request
90983 * would usually be made. Let's say we want to load data from http://domainB.com/users - the script tag that would be
90984 * injected might look like this:
90985 *
90986 * <script src="http://domainB.com/users?callback=someCallback"></script>
90987 *
90988 * When we inject the tag above, the browser makes a request to that url and includes the response as if it was any
90989 * other type of JavaScript include. By passing a callback in the url above, we're telling domainB's server that we want
90990 * to be notified when the result comes in and that it should call our callback function with the data it sends back. So
90991 * long as the server formats the response to look like this, everything will work:
90992 *
90993 * someCallback({
90994 * users: [
90995 * {
90996 * id: 1,
90997 * name: "Ed Spencer",
90998 * email: "ed@sencha.com"
90999 * }
91000 * ]
91001 * });
91002 *
91003 * As soon as the script finishes loading, the 'someCallback' function that we passed in the url is called with the JSON
91004 * object that the server returned.
91005 *
91006 * JsonP proxy takes care of all of this automatically. It formats the url you pass, adding the callback parameter
91007 * automatically. It even creates a temporary callback function, waits for it to be called and then puts the data into
91008 * the Proxy making it look just like you loaded it through a normal {@link Ext.data.proxy.Ajax AjaxProxy}. Here's how
91009 * we might set that up:
91010 *
91011 * Ext.define('User', {
91012 * extend: 'Ext.data.Model',
91013 * config: {
91014 * fields: ['id', 'name', 'email']
91015 * }
91016 * });
91017 *
91018 * var store = Ext.create('Ext.data.Store', {
91019 * model: 'User',
91020 * proxy: {
91021 * type: 'jsonp',
91022 * url : 'http://domainB.com/users'
91023 * }
91024 * });
91025 *
91026 * store.load();
91027 *
91028 * That's all we need to do - JsonP proxy takes care of the rest. In this case the Proxy will have injected a script tag
91029 * like this:
91030 *
91031 * <script src="http://domainB.com/users?callback=callback1"></script>
91032 *
91033 * # Customization
91034 *
91035 * This script tag can be customized using the {@link #callbackKey} configuration. For example:
91036 *
91037 * var store = Ext.create('Ext.data.Store', {
91038 * model: 'User',
91039 * proxy: {
91040 * type: 'jsonp',
91041 * url : 'http://domainB.com/users',
91042 * callbackKey: 'theCallbackFunction'
91043 * }
91044 * });
91045 *
91046 * store.load();
91047 *
91048 * Would inject a script tag like this:
91049 *
91050 * <script src="http://domainB.com/users?theCallbackFunction=callback1"></script>
91051 *
91052 * # Implementing on the server side
91053 *
91054 * The remote server side needs to be configured to return data in this format. Here are suggestions for how you might
91055 * achieve this using Java, PHP and ASP.net:
91056 *
91057 * Java:
91058 *
91059 * boolean jsonP = false;
91060 * String cb = request.getParameter("callback");
91061 * if (cb != null) {
91062 * jsonP = true;
91063 * response.setContentType("text/javascript");
91064 * } else {
91065 * response.setContentType("application/x-json");
91066 * }
91067 * Writer out = response.getWriter();
91068 * if (jsonP) {
91069 * out.write(cb + "(");
91070 * }
91071 * out.print(dataBlock.toJsonString());
91072 * if (jsonP) {
91073 * out.write(");");
91074 * }
91075 *
91076 * PHP:
91077 *
91078 * $callback = $_REQUEST['callback'];
91079 *
91080 * // Create the output object.
91081 * $output = array('a' => 'Apple', 'b' => 'Banana');
91082 *
91083 * //start output
91084 * if ($callback) {
91085 * header('Content-Type: text/javascript');
91086 * echo $callback . '(' . json_encode($output) . ');';
91087 * } else {
91088 * header('Content-Type: application/x-json');
91089 * echo json_encode($output);
91090 * }
91091 *
91092 * ASP.net:
91093 *
91094 * String jsonString = "{success: true}";
91095 * String cb = Request.Params.Get("callback");
91096 * String responseString = "";
91097 * if (!String.IsNullOrEmpty(cb)) {
91098 * responseString = cb + "(" + jsonString + ")";
91099 * } else {
91100 * responseString = jsonString;
91101 * }
91102 * Response.Write(responseString);
91103 *
91104 * ###Further Reading
91105 * [Sencha Touch Data Overview](../../../core_concepts/data/data_package_overview.html)
91106 * [Sencha Touch Store Guide](../../../core_concepts/data/stores.html)
91107 * [Sencha Touch Models Guide](../../../core_concepts/data/models.html)
91108 * [Sencha Touch Proxy Guide](../../../core_concepts/data/proxies.html)
91109 */
91110 Ext.define('Ext.data.proxy.JsonP', {
91111 extend: Ext.data.proxy.Server ,
91112 alternateClassName: 'Ext.data.ScriptTagProxy',
91113 alias: ['proxy.jsonp', 'proxy.scripttag'],
91114
91115
91116 config: {
91117 defaultWriterType: 'base',
91118
91119 /**
91120 * @cfg {String} callbackKey
91121 * See {@link Ext.data.JsonP#callbackKey}.
91122 * @accessor
91123 */
91124 callbackKey : 'callback',
91125
91126 /**
91127 * @cfg {String} recordParam
91128 * The param name to use when passing records to the server (e.g. 'records=someEncodedRecordString').
91129 * @accessor
91130 */
91131 recordParam: 'records',
91132
91133 /**
91134 * @cfg {Boolean} autoAppendParams
91135 * `true` to automatically append the request's params to the generated url.
91136 * @accessor
91137 */
91138 autoAppendParams: true
91139 },
91140
91141 /**
91142 * Performs the read request to the remote domain. JsonP proxy does not actually create an Ajax request,
91143 * instead we write out a `<script>` tag based on the configuration of the internal Ext.data.Request object
91144 * @param {Ext.data.Operation} operation The {@link Ext.data.Operation Operation} object to execute.
91145 * @param {Function} callback A callback function to execute when the Operation has been completed.
91146 * @param {Object} scope The scope to execute the callback in.
91147 * @return {Object}
91148 * @protected
91149 */
91150 doRequest: function(operation, callback, scope) {
91151 // <debug>
91152 var action = operation.getAction();
91153 if (action !== 'read') {
91154 Ext.Logger.error('JsonP proxies can only be used to read data.');
91155 }
91156 // </debug>
91157
91158 //generate the unique IDs for this request
91159 var me = this,
91160 request = me.buildRequest(operation),
91161 params = request.getParams();
91162
91163 // apply JsonP proxy-specific attributes to the Request
91164 request.setConfig({
91165 callbackKey: me.getCallbackKey(),
91166 timeout: me.getTimeout(),
91167 scope: me,
91168 callback: me.createRequestCallback(request, operation, callback, scope)
91169 });
91170
91171 // Prevent doubling up because the params are already added to the url in buildUrl
91172 if (me.getAutoAppendParams()) {
91173 request.setParams({});
91174 }
91175
91176 request.setJsonP(Ext.data.JsonP.request(request.getCurrentConfig()));
91177
91178 // Set the params back once we have made the request though
91179 request.setParams(params);
91180
91181 operation.setStarted();
91182
91183 me.lastRequest = request;
91184
91185 return request;
91186 },
91187
91188 /**
91189 * @private
91190 * Creates and returns the function that is called when the request has completed. The returned function
91191 * should accept a Response object, which contains the response to be read by the configured Reader.
91192 * The third argument is the callback that should be called after the request has been completed and the Reader has decoded
91193 * the response. This callback will typically be the callback passed by a store, e.g. in proxy.read(operation, theCallback, scope)
91194 * theCallback refers to the callback argument received by this function.
91195 * See {@link #doRequest} for details.
91196 * @param {Ext.data.Request} request The Request object.
91197 * @param {Ext.data.Operation} operation The Operation being executed.
91198 * @param {Function} callback The callback function to be called when the request completes. This is usually the callback
91199 * passed to doRequest.
91200 * @param {Object} scope The scope in which to execute the callback function.
91201 * @return {Function} The callback function.
91202 */
91203 createRequestCallback: function(request, operation, callback, scope) {
91204 var me = this;
91205
91206 return function(success, response, errorType) {
91207 delete me.lastRequest;
91208 me.processResponse(success, operation, request, response, callback, scope);
91209 };
91210 },
91211
91212 // @inheritdoc
91213 setException: function(operation, response) {
91214 operation.setException(operation.getRequest().getJsonP().errorType);
91215 },
91216
91217
91218 /**
91219 * Generates a url based on a given Ext.data.Request object. Adds the params and callback function name to the url
91220 * @param {Ext.data.Request} request The request object.
91221 * @return {String} The url.
91222 */
91223 buildUrl: function(request) {
91224 var me = this,
91225 url = me.callParent(arguments),
91226 params = Ext.apply({}, request.getParams()),
91227 filters = params.filters,
91228 filter, i, value;
91229
91230 delete params.filters;
91231
91232 if (me.getAutoAppendParams()) {
91233 url = Ext.urlAppend(url, Ext.Object.toQueryString(params));
91234 }
91235
91236 if (filters && filters.length) {
91237 for (i = 0; i < filters.length; i++) {
91238 filter = filters[i];
91239 value = filter.getValue();
91240 if (value) {
91241 url = Ext.urlAppend(url, filter.getProperty() + "=" + value);
91242 }
91243 }
91244 }
91245
91246 return url;
91247 },
91248
91249 /**
91250 * @inheritdoc
91251 */
91252 destroy: function() {
91253 this.abort();
91254 this.callParent(arguments);
91255 },
91256
91257 /**
91258 * Aborts the current server request if one is currently running.
91259 */
91260 abort: function() {
91261 var lastRequest = this.lastRequest;
91262 if (lastRequest) {
91263 Ext.data.JsonP.abort(lastRequest.getJsonP());
91264 }
91265 }
91266 });
91267
91268 /**
91269 * @author Ed Spencer
91270 *
91271 * WebStorageProxy is simply a superclass for the {@link Ext.data.proxy.LocalStorage LocalStorage} proxy. It uses the
91272 * new HTML5 key/value client-side storage objects to save {@link Ext.data.Model model instances} for offline use.
91273 * @private
91274 */
91275 Ext.define('Ext.data.proxy.WebStorage', {
91276 extend: Ext.data.proxy.Client ,
91277 alternateClassName: 'Ext.data.WebStorageProxy',
91278
91279
91280
91281 config: {
91282 /**
91283 * @cfg {String} id
91284 * The unique ID used as the key in which all record data are stored in the local storage object.
91285 */
91286 id: undefined,
91287
91288 // WebStorage proxies dont use readers and writers
91289 /**
91290 * @cfg
91291 * @hide
91292 */
91293 reader: null,
91294 /**
91295 * @cfg
91296 * @hide
91297 */
91298 writer: null,
91299
91300 /**
91301 * @cfg {Boolean} enablePagingParams This can be set to true if you want the webstorage proxy to comply
91302 * to the paging params set on the store.
91303 */
91304 enablePagingParams: false,
91305
91306 defaultDateFormat: 'Y-m-d H:i:s.u'
91307 },
91308
91309 /**
91310 * Creates the proxy, throws an error if local storage is not supported in the current browser.
91311 * @param {Object} config (optional) Config object.
91312 */
91313 constructor: function(config) {
91314 this.callParent(arguments);
91315
91316 /**
91317 * @property {Object} cache
91318 * Cached map of records already retrieved by this Proxy. Ensures that the same instance is always retrieved.
91319 */
91320 this.cache = {};
91321
91322 //<debug>
91323 if (this.getStorageObject() === undefined) {
91324 Ext.Logger.error("Local Storage is not supported in this browser, please use another type of data proxy");
91325 }
91326 //</debug>
91327 },
91328
91329 updateModel: function(model) {
91330 if (!this.getId()) {
91331 this.setId(model.modelName);
91332 }
91333
91334 this.callParent(arguments);
91335 },
91336
91337 //inherit docs
91338 create: function(operation, callback, scope) {
91339 var records = operation.getRecords(),
91340 length = records.length,
91341 ids = this.getIds(),
91342 id, record, i;
91343
91344 operation.setStarted();
91345
91346 for (i = 0; i < length; i++) {
91347 record = records[i];
91348 // <debug>
91349 if (!this.getModel().getIdentifier().isUnique) {
91350 Ext.Logger.warn('Your identifier generation strategy for the model does not ensure unique id\'s. Please use the UUID strategy, or implement your own identifier strategy with the flag isUnique.');
91351
91352 }
91353 // </debug>
91354 id = record.getId();
91355
91356 this.setRecord(record);
91357 ids.push(id);
91358 }
91359
91360 this.setIds(ids);
91361
91362 operation.setCompleted();
91363 operation.setSuccessful();
91364
91365 if (typeof callback == 'function') {
91366 callback.call(scope || this, operation);
91367 }
91368 },
91369
91370 //inherit docs
91371 read: function(operation, callback, scope) {
91372 var records = [],
91373 ids = this.getIds(),
91374 model = this.getModel(),
91375 idProperty = model.getIdProperty(),
91376 params = operation.getParams() || {},
91377 sorters = operation.getSorters(),
91378 filters = operation.getFilters(),
91379 start = operation.getStart(),
91380 limit = operation.getLimit(),
91381 length = ids.length,
91382 i, record, collection;
91383
91384 //read a single record
91385 if (params[idProperty] !== undefined) {
91386 record = this.getRecord(params[idProperty]);
91387 if (record) {
91388 records.push(record);
91389 operation.setSuccessful();
91390 }
91391 }
91392 else {
91393 for (i = 0; i < length; i++) {
91394 record = this.getRecord(ids[i]);
91395 if (record) {
91396 records.push(record);
91397 }
91398 }
91399
91400 collection = Ext.create('Ext.util.Collection');
91401
91402 // First we comply to filters
91403 if (filters && filters.length) {
91404 collection.setFilters(filters);
91405 }
91406 // Then we comply to sorters
91407 if (sorters && sorters.length) {
91408 collection.setSorters(sorters);
91409 }
91410
91411 collection.addAll(records);
91412
91413 if (this.getEnablePagingParams() && start !== undefined && limit !== undefined) {
91414 records = collection.items.slice(start, start + limit);
91415 } else {
91416 records = collection.items.slice();
91417 }
91418
91419 operation.setSuccessful();
91420 }
91421
91422 operation.setCompleted();
91423
91424 operation.setResultSet(Ext.create('Ext.data.ResultSet', {
91425 records: records,
91426 total : records.length,
91427 loaded : true
91428 }));
91429 operation.setRecords(records);
91430
91431 if (typeof callback == 'function') {
91432 callback.call(scope || this, operation);
91433 }
91434 },
91435
91436 //inherit docs
91437 update: function(operation, callback, scope) {
91438 var records = operation.getRecords(),
91439 length = records.length,
91440 ids = this.getIds(),
91441 record, id, i;
91442
91443 operation.setStarted();
91444
91445 for (i = 0; i < length; i++) {
91446 record = records[i];
91447 this.setRecord(record);
91448
91449 //we need to update the set of ids here because it's possible that a non-phantom record was added
91450 //to this proxy - in which case the record's id would never have been added via the normal 'create' call
91451 id = record.getId();
91452 if (id !== undefined && Ext.Array.indexOf(ids, id) == -1) {
91453 ids.push(id);
91454 }
91455 }
91456 this.setIds(ids);
91457
91458 operation.setCompleted();
91459 operation.setSuccessful();
91460
91461 if (typeof callback == 'function') {
91462 callback.call(scope || this, operation);
91463 }
91464 },
91465
91466 //inherit
91467 destroy: function(operation, callback, scope) {
91468 var records = operation.getRecords(),
91469 length = records.length,
91470 ids = this.getIds(),
91471
91472 //newIds is a copy of ids, from which we remove the destroyed records
91473 newIds = [].concat(ids),
91474 i;
91475
91476 operation.setStarted();
91477
91478 for (i = 0; i < length; i++) {
91479 Ext.Array.remove(newIds, records[i].getId());
91480 this.removeRecord(records[i], false);
91481 }
91482
91483 this.setIds(newIds);
91484
91485 operation.setCompleted();
91486 operation.setSuccessful();
91487
91488 if (typeof callback == 'function') {
91489 callback.call(scope || this, operation);
91490 }
91491 },
91492
91493 /**
91494 * @private
91495 * Fetches a model instance from the Proxy by ID. Runs each field's decode function (if present) to decode the data.
91496 * @param {String} id The record's unique ID
91497 * @return {Ext.data.Model} The model instance or undefined if the record did not exist in the storage.
91498 */
91499 getRecord: function(id) {
91500 if (this.cache[id] === undefined) {
91501 var recordKey = this.getRecordKey(id),
91502 item = this.getStorageObject().getItem(recordKey),
91503 data = {},
91504 Model = this.getModel(),
91505 fields = Model.getFields().items,
91506 length = fields.length,
91507 i, field, name, record, rawData, rawValue;
91508
91509 if (!item) {
91510 return undefined;
91511 }
91512
91513 rawData = Ext.decode(item);
91514
91515 for (i = 0; i < length; i++) {
91516 field = fields[i];
91517 name = field.getName();
91518 rawValue = rawData[name];
91519
91520 if (typeof field.getDecode() == 'function') {
91521 data[name] = field.getDecode()(rawValue);
91522 } else {
91523 if (field.getType().type == 'date') {
91524 data[name] = this.readDate(field, rawValue);
91525 } else {
91526 data[name] = rawValue;
91527 }
91528 }
91529 }
91530
91531 record = new Model(data, id);
91532 this.cache[id] = record;
91533 }
91534
91535 return this.cache[id];
91536 },
91537
91538 /**
91539 * Saves the given record in the Proxy. Runs each field's encode function (if present) to encode the data.
91540 * @param {Ext.data.Model} record The model instance
91541 * @param {String} [id] The id to save the record under (defaults to the value of the record's getId() function)
91542 */
91543 setRecord: function(record, id) {
91544 if (id) {
91545 record.setId(id);
91546 } else {
91547 id = record.getId();
91548 }
91549
91550 var me = this,
91551 rawData = record.getData(),
91552 data = {},
91553 Model = me.getModel(),
91554 fields = Model.getFields().items,
91555 length = fields.length,
91556 i = 0,
91557 rawValue, field, name, obj, key;
91558
91559 for (; i < length; i++) {
91560 field = fields[i];
91561 name = field.getName();
91562 rawValue = rawData[name];
91563
91564 if (field.getPersist() === false) {
91565 continue;
91566 }
91567
91568 if (typeof field.getEncode() == 'function') {
91569 data[name] = field.getEncode()(rawValue, record);
91570 } else {
91571 if (field.getType().type == 'date' && Ext.isDate(rawValue)) {
91572 data[name] = this.writeDate(field, rawValue);
91573 } else {
91574 data[name] = rawValue;
91575 }
91576 }
91577 }
91578
91579 obj = me.getStorageObject();
91580 key = me.getRecordKey(id);
91581
91582 //keep the cache up to date
91583 me.cache[id] = record;
91584
91585 //iPad bug requires that we remove the item before setting it
91586 obj.removeItem(key);
91587 try {
91588 obj.setItem(key, Ext.encode(data));
91589 } catch(e){
91590 this.fireEvent('exception', this, e);
91591 }
91592
91593 record.commit();
91594 },
91595
91596 /**
91597 * @private
91598 * Physically removes a given record from the local storage. Used internally
91599 * by {@link #destroy}, which you should use instead because it updates the
91600 * list of currently-stored record ids.
91601 * @param {String/Number/Ext.data.Model} id The id of the record to remove,
91602 * or an Ext.data.Model instance.
91603 * @param {Boolean} [updateIds] False to skip saving the array of ids
91604 * representing the set of all records in the Proxy.
91605 */
91606 removeRecord: function(id, updateIds) {
91607 var me = this,
91608 ids;
91609
91610 if (id.isModel) {
91611 id = id.getId();
91612 }
91613
91614 if (updateIds !== false) {
91615 ids = me.getIds();
91616 Ext.Array.remove(ids, id);
91617 me.setIds(ids);
91618 }
91619
91620 delete this.cache[id];
91621 me.getStorageObject().removeItem(me.getRecordKey(id));
91622 },
91623
91624 /**
91625 * @private
91626 * Given the id of a record, returns a unique string based on that id and the id of this proxy. This is used when
91627 * storing data in the local storage object and should prevent naming collisions.
91628 * @param {String/Number/Ext.data.Model} id The record id, or a Model instance
91629 * @return {String} The unique key for this record
91630 */
91631 getRecordKey: function(id) {
91632 if (id.isModel) {
91633 id = id.getId();
91634 }
91635
91636 return Ext.String.format("{0}-{1}", this.getId(), id);
91637 },
91638
91639 /**
91640 * @private
91641 * Returns the array of record IDs stored in this Proxy
91642 * @return {Number[]} The record IDs. Each is cast as a Number
91643 */
91644 getIds: function() {
91645 var ids = (this.getStorageObject().getItem(this.getId()) || "").split(","),
91646 length = ids.length,
91647 i;
91648
91649 if (length == 1 && ids[0] === "") {
91650 ids = [];
91651 }
91652
91653 return ids;
91654 },
91655
91656 /**
91657 * @private
91658 * Saves the array of ids representing the set of all records in the Proxy
91659 * @param {Number[]} ids The ids to set
91660 */
91661 setIds: function(ids) {
91662 var obj = this.getStorageObject(),
91663 str = ids.join(","),
91664 id = this.getId();
91665
91666 obj.removeItem(id);
91667
91668 if (!Ext.isEmpty(str)) {
91669 try {
91670 obj.setItem(id, str);
91671 } catch(e){
91672 this.fireEvent('exception', this, e);
91673 }
91674 }
91675 },
91676
91677 writeDate: function(field, date) {
91678 if (Ext.isEmpty(date)) {
91679 return null;
91680 }
91681
91682 var dateFormat = field.getDateFormat() || this.getDefaultDateFormat();
91683 switch (dateFormat) {
91684 case 'timestamp':
91685 return date.getTime() / 1000;
91686 case 'time':
91687 return date.getTime();
91688 default:
91689 return Ext.Date.format(date, dateFormat);
91690 }
91691 },
91692
91693 readDate: function(field, date) {
91694 if (Ext.isEmpty(date)) {
91695 return null;
91696 }
91697
91698 var dateFormat = field.getDateFormat() || this.getDefaultDateFormat();
91699 switch (dateFormat) {
91700 case 'timestamp':
91701 return new Date(date * 1000);
91702 case 'time':
91703 return new Date(date);
91704 default:
91705 return Ext.isDate(date) ? date : Ext.Date.parse(date, dateFormat);
91706 }
91707 },
91708
91709 /**
91710 * @private
91711 * Sets up the Proxy by claiming the key in the storage object that corresponds to the unique id of this Proxy. Called
91712 * automatically by the constructor, this should not need to be called again unless {@link #clear} has been called.
91713 */
91714 initialize: function() {
91715 this.callParent(arguments);
91716 var storageObject = this.getStorageObject();
91717 try {
91718 storageObject.setItem(this.getId(), storageObject.getItem(this.getId()) || "");
91719 } catch(e){
91720 this.fireEvent('exception', this, e);
91721 }
91722 },
91723
91724 /**
91725 * Destroys all records stored in the proxy and removes all keys and values used to support the proxy from the
91726 * storage object.
91727 */
91728 clear: function() {
91729 var obj = this.getStorageObject(),
91730 ids = this.getIds(),
91731 len = ids.length,
91732 i;
91733
91734 //remove all the records
91735 for (i = 0; i < len; i++) {
91736 this.removeRecord(ids[i], false);
91737 }
91738
91739 //remove the supporting objects
91740 obj.removeItem(this.getId());
91741 },
91742
91743 /**
91744 * @private
91745 * Abstract function which should return the storage object that data will be saved to. This must be implemented
91746 * in each subclass.
91747 * @return {Object} The storage object
91748 */
91749 getStorageObject: function() {
91750 //<debug>
91751 Ext.Logger.error("The getStorageObject function has not been defined in your Ext.data.proxy.WebStorage subclass");
91752 //</debug>
91753 }
91754 });
91755
91756 /**
91757 * @author Ed Spencer
91758 *
91759 * The LocalStorageProxy uses the new HTML5 localStorage API to save {@link Ext.data.Model Model} data locally on the
91760 * client browser. HTML5 localStorage is a key-value store (e.g. cannot save complex objects like JSON), so
91761 * LocalStorageProxy automatically serializes and deserializes data when saving and retrieving it.
91762 *
91763 * localStorage is extremely useful for saving user-specific information without needing to build server-side
91764 * infrastructure to support it. Let's imagine we're writing a Twitter search application and want to save the user's
91765 * searches locally so they can easily perform a saved search again later. We'd start by creating a Search model:
91766 *
91767 * Ext.define('Search', {
91768 * extend: 'Ext.data.Model',
91769 * config: {
91770 * fields: ['id', 'query'],
91771 * proxy: {
91772 * type: 'localstorage',
91773 * id : 'twitter-Searches'
91774 * }
91775 * }
91776 * });
91777 *
91778 * Our Search model contains just two fields - id and query - plus a Proxy definition. The only configuration we need to
91779 * pass to the LocalStorage proxy is an {@link #id}. This is important as it separates the Model data in this Proxy from
91780 * all others. The localStorage API puts all data into a single shared namespace, so by setting an id we enable
91781 * LocalStorageProxy to manage the saved Search data.
91782 *
91783 * Saving our data into localStorage is easy and would usually be done with a {@link Ext.data.Store Store}:
91784 *
91785 * //our Store automatically picks up the LocalStorageProxy defined on the Search model
91786 * var store = Ext.create('Ext.data.Store', {
91787 * model: "Search"
91788 * });
91789 *
91790 * //loads any existing Search data from localStorage
91791 * store.load();
91792 *
91793 * //now add some Searches
91794 * store.add({query: 'Sencha Touch'});
91795 * store.add({query: 'Ext JS'});
91796 *
91797 * //finally, save our Search data to localStorage
91798 * store.sync();
91799 *
91800 * The LocalStorageProxy automatically gives our new Searches an id when we call store.sync(). It encodes the Model data
91801 * and places it into localStorage. We can also save directly to localStorage, bypassing the Store altogether:
91802 *
91803 * var search = Ext.create('Search', {query: 'Sencha Animator'});
91804 *
91805 * //uses the configured LocalStorageProxy to save the new Search to localStorage
91806 * search.save();
91807 *
91808 * # Limitations
91809 *
91810 * If this proxy is used in a browser where local storage is not supported, the constructor will throw an error. A local
91811 * storage proxy requires a unique ID which is used as a key in which all record data are stored in the local storage
91812 * object.
91813 *
91814 * It's important to supply this unique ID as it cannot be reliably determined otherwise. If no id is provided but the
91815 * attached store has a storeId, the storeId will be used. If neither option is presented the proxy will throw an error.
91816 *
91817 * ###Further Reading
91818 * [Sencha Touch Data Overview](../../../core_concepts/data/data_package_overview.html)
91819 * [Sencha Touch Store Guide](../../../core_concepts/data/stores.html)
91820 * [Sencha Touch Models Guide](../../../core_concepts/data/models.html)
91821 * [Sencha Touch Proxy Guide](../../../core_concepts/data/proxies.html)
91822 */
91823 Ext.define('Ext.data.proxy.LocalStorage', {
91824 extend: Ext.data.proxy.WebStorage ,
91825 alias: 'proxy.localstorage',
91826 alternateClassName: 'Ext.data.LocalStorageProxy',
91827
91828 //inherit docs
91829 getStorageObject: function() {
91830 return window.localStorage;
91831 }
91832 });
91833
91834 /**
91835 * @author Ed Spencer
91836 *
91837 * The Rest proxy is a specialization of the {@link Ext.data.proxy.Ajax AjaxProxy} which simply maps the four actions
91838 * (create, read, update and destroy) to RESTful HTTP verbs. For example, let's set up a {@link Ext.data.Model Model}
91839 * with an inline Rest proxy:
91840 *
91841 * Ext.define('User', {
91842 * extend: 'Ext.data.Model',
91843 * config: {
91844 * fields: ['id', 'name', 'email'],
91845 *
91846 * proxy: {
91847 * type: 'rest',
91848 * url : '/users'
91849 * }
91850 * }
91851 * });
91852 *
91853 * Now we can create a new User instance and save it via the Rest proxy. Doing this will cause the Proxy to send a POST
91854 * request to '/users':
91855 *
91856 * var user = Ext.create('User', {name: 'Ed Spencer', email: 'ed@sencha.com'});
91857 *
91858 * user.save(); //POST /users
91859 *
91860 * Let's expand this a little and provide a callback for the {@link Ext.data.Model#save} call to update the Model once
91861 * it has been created. We'll assume the creation went successfully and that the server gave this user an ID of 123:
91862 *
91863 * user.save({
91864 * success: function(user) {
91865 * user.set('name', 'Khan Noonien Singh');
91866 *
91867 * user.save(); //PUT /users/123
91868 * }
91869 * });
91870 *
91871 * Now that we're no longer creating a new Model instance, the request method is changed to an HTTP PUT, targeting the
91872 * relevant url for that user. Now let's delete this user, which will use the DELETE method:
91873 *
91874 * user.erase(); //DELETE /users/123
91875 *
91876 * Finally, when we perform a load of a Model or Store, Rest proxy will use the GET method:
91877 *
91878 * //1. Load via Store
91879 *
91880 * //the Store automatically picks up the Proxy from the User model
91881 * var store = Ext.create('Ext.data.Store', {
91882 * model: 'User'
91883 * });
91884 *
91885 * store.load(); //GET /users
91886 *
91887 * //2. Load directly from the Model
91888 *
91889 * //GET /users/123
91890 * Ext.ModelManager.getModel('User').load(123, {
91891 * success: function(user) {
91892 * console.log(user.getId()); //outputs 123
91893 * }
91894 * });
91895 *
91896 * # Url generation
91897 *
91898 * The Rest proxy is able to automatically generate the urls above based on two configuration options - {@link #appendId} and
91899 * {@link #format}. If appendId is true (it is by default) then Rest proxy will automatically append the ID of the Model
91900 * instance in question to the configured url, resulting in the '/users/123' that we saw above.
91901 *
91902 * If the request is not for a specific Model instance (e.g. loading a Store), the url is not appended with an id.
91903 * The Rest proxy will automatically insert a '/' before the ID if one is not already present.
91904 *
91905 * new Ext.data.proxy.Rest({
91906 * url: '/users',
91907 * appendId: true //default
91908 * });
91909 *
91910 * // Collection url: /users
91911 * // Instance url : /users/123
91912 *
91913 * The Rest proxy can also optionally append a format string to the end of any generated url:
91914 *
91915 * new Ext.data.proxy.Rest({
91916 * url: '/users',
91917 * format: 'json'
91918 * });
91919 *
91920 * // Collection url: /users.json
91921 * // Instance url : /users/123.json
91922 *
91923 * If further customization is needed, simply implement the {@link #buildUrl} method and add your custom generated url
91924 * onto the {@link Ext.data.Request Request} object that is passed to buildUrl. See [Rest proxy's implementation][1] for
91925 * an example of how to achieve this.
91926 *
91927 * Note that Rest proxy inherits from {@link Ext.data.proxy.Ajax AjaxProxy}, which already injects all of the sorter,
91928 * filter, group and paging options into the generated url. See the {@link Ext.data.proxy.Ajax AjaxProxy docs} for more
91929 * details.
91930 *
91931 * [1]: source/Rest.html#Ext-data-proxy-Rest-method-buildUrl
91932 *
91933 * ###Further Reading
91934 * [Sencha Touch Data Overview](../../../core_concepts/data/data_package_overview.html)
91935 * [Sencha Touch Store Guide](../../../core_concepts/data/stores.html)
91936 * [Sencha Touch Models Guide](../../../core_concepts/data/models.html)
91937 * [Sencha Touch Proxy Guide](../../../core_concepts/data/proxies.html)
91938 */
91939 Ext.define('Ext.data.proxy.Rest', {
91940 extend: Ext.data.proxy.Ajax ,
91941 alternateClassName: 'Ext.data.RestProxy',
91942 alias : 'proxy.rest',
91943
91944 config: {
91945 /**
91946 * @cfg {Boolean} appendId
91947 * `true` to automatically append the ID of a Model instance when performing a request based on that single instance.
91948 * See Rest proxy intro docs for more details.
91949 */
91950 appendId: true,
91951
91952 /**
91953 * @cfg {String} format
91954 * Optional data format to send to the server when making any request (e.g. 'json'). See the Rest proxy intro docs
91955 * for full details.
91956 */
91957 format: null,
91958
91959 /**
91960 * @cfg {Boolean} batchActions
91961 * `true` to batch actions of a particular type when synchronizing the store.
91962 */
91963 batchActions: false,
91964
91965 actionMethods: {
91966 create : 'POST',
91967 read : 'GET',
91968 update : 'PUT',
91969 destroy: 'DELETE'
91970 }
91971 },
91972
91973 /**
91974 * Specialized version of `buildUrl` that incorporates the {@link #appendId} and {@link #format} options into the
91975 * generated url. Override this to provide further customizations, but remember to call the superclass `buildUrl` so
91976 * that additional parameters like the cache buster string are appended.
91977 * @param {Object} request
91978 * @return {Object}
91979 */
91980 buildUrl: function(request) {
91981 var me = this,
91982 operation = request.getOperation(),
91983 records = operation.getRecords() || [],
91984 record = records[0],
91985 model = me.getModel(),
91986 idProperty= model.getIdProperty(),
91987 format = me.getFormat(),
91988 url = me.getUrl(request),
91989 params = request.getParams() || {},
91990 id = (record && !record.phantom) ? record.getId() : params[idProperty];
91991
91992 if (me.getAppendId() && id) {
91993 if (!url.match(/\/$/)) {
91994 url += '/';
91995 }
91996 url += id;
91997 delete params[idProperty];
91998 }
91999
92000 if (format) {
92001 if (!url.match(/\.$/)) {
92002 url += '.';
92003 }
92004
92005 url += format;
92006 }
92007
92008 request.setUrl(url);
92009
92010 return me.callParent([request]);
92011 }
92012 });
92013
92014 /**
92015 * @author Ed Spencer
92016 *
92017 * Proxy which uses HTML5 session storage as its data storage/retrieval mechanism. If this proxy is used in a browser
92018 * where session storage is not supported, the constructor will throw an error. A session storage proxy requires a
92019 * unique ID which is used as a key in which all record data are stored in the session storage object.
92020 *
92021 * It's important to supply this unique ID as it cannot be reliably determined otherwise. If no id is provided but the
92022 * attached store has a storeId, the storeId will be used. If neither option is presented the proxy will throw an error.
92023 *
92024 * Proxies are almost always used with a {@link Ext.data.Store store}:
92025 *
92026 * new Ext.data.Store({
92027 * proxy: {
92028 * type: 'sessionstorage',
92029 * id : 'myProxyKey'
92030 * }
92031 * });
92032 *
92033 * Alternatively you can instantiate the Proxy directly:
92034 *
92035 * new Ext.data.proxy.SessionStorage({
92036 * id : 'myOtherProxyKey'
92037 * });
92038 *
92039 * Note that session storage is different to local storage (see {@link Ext.data.proxy.LocalStorage}) - if a browser
92040 * session is ended (e.g. by closing the browser) then all data in a SessionStorageProxy are lost. Browser restarts
92041 * don't affect the {@link Ext.data.proxy.LocalStorage} - the data are preserved.
92042 *
92043 * ###Further Reading
92044 * [Sencha Touch Data Overview](../../../core_concepts/data/data_package_overview.html)
92045 * [Sencha Touch Store Guide](../../../core_concepts/data/stores.html)
92046 * [Sencha Touch Models Guide](../../../core_concepts/data/models.html)
92047 * [Sencha Touch Proxy Guide](../../../core_concepts/data/proxies.html)
92048 */
92049 Ext.define('Ext.data.proxy.SessionStorage', {
92050 extend: Ext.data.proxy.WebStorage ,
92051 alias: 'proxy.sessionstorage',
92052 alternateClassName: 'Ext.data.SessionStorageProxy',
92053
92054 //inherit docs
92055 getStorageObject: function() {
92056 return window.sessionStorage;
92057 }
92058 });
92059
92060 /**
92061 * SQL proxy lets you store data in a SQL database.
92062 * The Sencha Touch SQL proxy outputs model data into an HTML5
92063 * local database using WebSQL.
92064 *
92065 * You can create a Store for the proxy, for example:
92066 *
92067 * Ext.require(["Ext.data.proxy.SQL"]);
92068 *
92069 * Ext.define("User", {
92070 * extend: "Ext.data.Model",
92071 * config: {
92072 * fields: [ "firstName", "lastName" ]
92073 * }
92074 * });
92075 *
92076 * Ext.create("Ext.data.Store", {
92077 * model: "User",
92078 * storeId: "Users",
92079 * proxy: {
92080 * type: "sql"
92081 * }
92082 * });
92083 *
92084 * Ext.getStore("Users").add({
92085 * firstName: "Polly",
92086 * lastName: "Hedra"
92087 * });
92088 *
92089 * Ext.getStore("Users").sync();
92090 *
92091 * To destroy a table use:
92092 *
92093 * Ext.getStore("Users").getProxy().dropTable();
92094 *
92095 * To recreate a table use:
92096 *
92097 * Ext.data.Store.sync() or Ext.data.Model.save()
92098 */
92099 Ext.define('Ext.data.proxy.Sql', {
92100 alias: 'proxy.sql',
92101 extend: Ext.data.proxy.Client ,
92102 alternateClassName: 'Ext.data.proxy.SQL',
92103
92104 isSQLProxy: true,
92105
92106 config: {
92107 /**
92108 * @cfg {Object} reader
92109 * @hide
92110 */
92111 reader: null,
92112 /**
92113 * @cfg {Object} writer
92114 * @hide
92115 */
92116 writer: null,
92117 /**
92118 * @cfg {String} table
92119 * Optional Table name to use if not provided ModelName will be used
92120 */
92121 table: null,
92122 /**
92123 * @cfg {String} database
92124 * Database name to access tables from
92125 */
92126 database: 'Sencha',
92127
92128 columns: '',
92129
92130 uniqueIdStrategy: false,
92131
92132 tableExists: false,
92133
92134 defaultDateFormat: 'Y-m-d H:i:s.u'
92135 },
92136
92137 updateModel: function(model) {
92138 if (model) {
92139 var modelName = model.modelName,
92140 defaultDateFormat = this.getDefaultDateFormat(),
92141 table = modelName.slice(modelName.lastIndexOf('.') + 1);
92142
92143 model.getFields().each(function (field) {
92144 if (field.getType().type === 'date' && !field.getDateFormat()) {
92145 field.setDateFormat(defaultDateFormat);
92146 }
92147 });
92148
92149 this.setUniqueIdStrategy(model.getIdentifier().isUnique);
92150 if (!this.getTable()) {
92151 this.setTable(table);
92152 }
92153 this.setColumns(this.getPersistedModelColumns(model));
92154 }
92155
92156 this.callParent(arguments);
92157 },
92158
92159 setException: function(operation, error) {
92160 operation.setException(error);
92161 },
92162
92163 create: function (operation, callback, scope) {
92164 var me = this,
92165 db = me.getDatabaseObject(),
92166 records = operation.getRecords(),
92167 tableExists = me.getTableExists();
92168
92169 operation.setStarted();
92170
92171 db.transaction(function(transaction) {
92172 if (!tableExists) {
92173 me.createTable(transaction);
92174 }
92175
92176 me.insertRecords(records, transaction, function(resultSet, error) {
92177 if (operation.process(operation.getAction(), resultSet) === false) {
92178 me.fireEvent('exception', me, operation);
92179 }
92180
92181 if (error) {
92182 operation.setException(error);
92183 }
92184 }, me);
92185 },
92186 function(transaction, error) {
92187 me.setException(operation, error);
92188 if (typeof callback == 'function') {
92189 callback.call(scope || me, operation);
92190 }
92191 },
92192 function(transaction) {
92193 if (typeof callback == 'function') {
92194 callback.call(scope || me, operation);
92195 }
92196 }
92197 );
92198 },
92199
92200 read: function(operation, callback, scope) {
92201 var me = this,
92202 db = me.getDatabaseObject(),
92203 model = me.getModel(),
92204 idProperty = model.getIdProperty(),
92205 tableExists = me.getTableExists(),
92206 params = operation.getParams() || {},
92207 id = params[idProperty],
92208 sorters = operation.getSorters(),
92209 filters = operation.getFilters(),
92210 page = operation.getPage(),
92211 start = operation.getStart(),
92212 limit = operation.getLimit(),
92213 filtered, i, ln;
92214
92215 params = Ext.apply(params, {
92216 page: page,
92217 start: start,
92218 limit: limit,
92219 sorters: sorters,
92220 filters: filters
92221 });
92222
92223 operation.setStarted();
92224
92225 db.transaction(function(transaction) {
92226 if (!tableExists) {
92227 me.createTable(transaction);
92228 }
92229
92230 me.selectRecords(transaction, id !== undefined ? id : params, function (resultSet, error) {
92231 if (operation.process(operation.getAction(), resultSet) === false) {
92232 me.fireEvent('exception', me, operation);
92233 }
92234
92235 if (error) {
92236 operation.setException(error);
92237 }
92238
92239 if (filters && filters.length) {
92240 filtered = Ext.create('Ext.util.Collection', function(record) {
92241 return record.getId();
92242 });
92243 filtered.setFilterRoot('data');
92244 for (i = 0, ln = filters.length; i < ln; i++) {
92245 if (filters[i].getProperty() === null) {
92246 filtered.addFilter(filters[i]);
92247 }
92248 }
92249 filtered.addAll(operation.getRecords());
92250
92251 operation.setRecords(filtered.items.slice());
92252 resultSet.setRecords(operation.getRecords());
92253 resultSet.setCount(filtered.items.length);
92254 resultSet.setTotal(filtered.items.length);
92255 }
92256 });
92257 },
92258 function(transaction, error) {
92259 me.setException(operation, error);
92260 if (typeof callback == 'function') {
92261 callback.call(scope || me, operation);
92262 }
92263 },
92264 function(transaction) {
92265 if (typeof callback == 'function') {
92266 callback.call(scope || me, operation);
92267 }
92268 }
92269 );
92270 },
92271
92272 update: function(operation, callback, scope) {
92273 var me = this,
92274 records = operation.getRecords(),
92275 db = me.getDatabaseObject(),
92276 tableExists = me.getTableExists();
92277
92278 operation.setStarted();
92279
92280 db.transaction(function (transaction) {
92281 if (!tableExists) {
92282 me.createTable(transaction);
92283 }
92284
92285 me.updateRecords(transaction, records, function(resultSet, errors) {
92286 if (operation.process(operation.getAction(), resultSet) === false) {
92287 me.fireEvent('exception', me, operation);
92288 }
92289
92290 if (errors) {
92291 operation.setException(errors);
92292 }
92293 });
92294 },
92295 function(transaction, error) {
92296 me.setException(operation, error);
92297 if (typeof callback == 'function') {
92298 callback.call(scope || me, operation);
92299 }
92300 },
92301 function(transaction) {
92302 if (typeof callback == 'function') {
92303 callback.call(scope || me, operation);
92304 }
92305 }
92306 );
92307 },
92308
92309 destroy: function(operation, callback, scope) {
92310 var me = this,
92311 records = operation.getRecords(),
92312 db = me.getDatabaseObject(),
92313 tableExists = me.getTableExists();
92314
92315 operation.setStarted();
92316
92317 db.transaction(function(transaction) {
92318 if (!tableExists) {
92319 me.createTable(transaction);
92320 }
92321
92322 me.destroyRecords(transaction, records, function(resultSet, error) {
92323 if (operation.process(operation.getAction(), resultSet) === false) {
92324 me.fireEvent('exception', me, operation);
92325 }
92326
92327 if (error) {
92328 operation.setException(error);
92329 }
92330 });
92331 },
92332 function(transaction, error) {
92333 me.setException(operation, error);
92334 if (typeof callback == 'function') {
92335 callback.call(scope || me, operation);
92336 }
92337 },
92338 function(transaction) {
92339 if (typeof callback == 'function') {
92340 callback.call(scope || me, operation);
92341 }
92342 }
92343 );
92344 },
92345
92346 createTable: function (transaction) {
92347 transaction.executeSql('CREATE TABLE IF NOT EXISTS ' + this.getTable() + ' (' + this.getSchemaString() + ')');
92348 this.setTableExists(true);
92349 },
92350
92351 insertRecords: function(records, transaction, callback, scope) {
92352 var me = this,
92353 table = me.getTable(),
92354 columns = me.getColumns(),
92355 totalRecords = records.length,
92356 executed = 0,
92357 tmp = [],
92358 insertedRecords = [],
92359 errors = [],
92360 uniqueIdStrategy = me.getUniqueIdStrategy(),
92361 i, ln, placeholders, result;
92362
92363 result = new Ext.data.ResultSet({
92364 records: insertedRecords,
92365 success: true
92366 });
92367
92368 for (i = 0, ln = columns.length; i < ln; i++) {
92369 tmp.push('?');
92370 }
92371 placeholders = tmp.join(', ');
92372
92373 Ext.each(records, function (record) {
92374 var id = record.getId(),
92375 data = me.getRecordData(record),
92376 values = me.getColumnValues(columns, data);
92377
92378 transaction.executeSql(
92379 'INSERT INTO ' + table + ' (' + columns.join(', ') + ') VALUES (' + placeholders + ')', values,
92380 function (transaction, resultSet) {
92381 executed++;
92382 insertedRecords.push({
92383 clientId: id,
92384 id: uniqueIdStrategy ? id : resultSet.insertId,
92385 data: data,
92386 node: data
92387 });
92388
92389 if (executed === totalRecords && typeof callback == 'function') {
92390 callback.call(scope || me, result, errors.length > 0 ? errors : null);
92391 }
92392 },
92393 function (transaction, error) {
92394 executed++;
92395 errors.push({
92396 clientId: id,
92397 error: error
92398 });
92399
92400 if (executed === totalRecords && typeof callback == 'function') {
92401 callback.call(scope || me, result, errors);
92402 }
92403 }
92404 );
92405 });
92406 },
92407
92408 selectRecords: function(transaction, params, callback, scope) {
92409 var me = this,
92410 table = me.getTable(),
92411 idProperty = me.getModel().getIdProperty(),
92412 sql = 'SELECT * FROM ' + table,
92413 records = [],
92414 filterStatement = ' WHERE ',
92415 sortStatement = ' ORDER BY ',
92416 i, ln, data, result, count, rows, filter, sorter, property, value;
92417
92418 result = new Ext.data.ResultSet({
92419 records: records,
92420 success: true
92421 });
92422
92423 if (!Ext.isObject(params)) {
92424 sql += filterStatement + idProperty + ' = ' + params;
92425 } else {
92426 ln = params.filters && params.filters.length;
92427 if (ln) {
92428 for (i = 0; i < ln; i++) {
92429 filter = params.filters[i];
92430 property = filter.getProperty();
92431 value = filter.getValue();
92432 if (property !== null) {
92433 sql += filterStatement + property + ' ' + (filter.getAnyMatch() ? ('LIKE \'%' + value + '%\'') : ('= \'' + value + '\''));
92434 filterStatement = ' AND ';
92435 }
92436 }
92437 }
92438
92439 ln = params.sorters && params.sorters.length;
92440 if (ln) {
92441 for (i = 0; i < ln; i++) {
92442 sorter = params.sorters[i];
92443 property = sorter.getProperty();
92444 if (property !== null) {
92445 sql += sortStatement + property + ' ' + sorter.getDirection();
92446 sortStatement = ', ';
92447 }
92448 }
92449 }
92450
92451 // handle start, limit, sort, filter and group params
92452 if (params.page !== undefined) {
92453 sql += ' LIMIT ' + parseInt(params.start, 10) + ', ' + parseInt(params.limit, 10);
92454 }
92455 }
92456 transaction.executeSql(sql, null,
92457 function(transaction, resultSet) {
92458 rows = resultSet.rows;
92459 count = rows.length;
92460
92461 for (i = 0, ln = count; i < ln; i++) {
92462 data = rows.item(i);
92463 records.push({
92464 clientId: null,
92465 id: data[idProperty],
92466 data: data,
92467 node: data
92468 });
92469 }
92470
92471 result.setSuccess(true);
92472 result.setTotal(count);
92473 result.setCount(count);
92474
92475 if (typeof callback == 'function') {
92476 callback.call(scope || me, result);
92477 }
92478 },
92479 function(transaction, error) {
92480 result.setSuccess(false);
92481 result.setTotal(0);
92482 result.setCount(0);
92483
92484 if (typeof callback == 'function') {
92485 callback.call(scope || me, result, error);
92486 }
92487 }
92488 );
92489 },
92490
92491 updateRecords: function (transaction, records, callback, scope) {
92492 var me = this,
92493 table = me.getTable(),
92494 columns = me.getColumns(),
92495 totalRecords = records.length,
92496 idProperty = me.getModel().getIdProperty(),
92497 executed = 0,
92498 updatedRecords = [],
92499 errors = [],
92500 i, ln, result;
92501
92502 result = new Ext.data.ResultSet({
92503 records: updatedRecords,
92504 success: true
92505 });
92506
92507 Ext.each(records, function (record) {
92508 var id = record.getId(),
92509 data = me.getRecordData(record),
92510 values = me.getColumnValues(columns, data),
92511 updates = [];
92512
92513 for (i = 0, ln = columns.length; i < ln; i++) {
92514 updates.push(columns[i] + ' = ?');
92515 }
92516
92517 transaction.executeSql(
92518 'UPDATE ' + table + ' SET ' + updates.join(', ') + ' WHERE ' + idProperty + ' = ?', values.concat(id),
92519 function (transaction, resultSet) {
92520 executed++;
92521 updatedRecords.push({
92522 clientId: id,
92523 id: id,
92524 data: data,
92525 node: data
92526 });
92527
92528 if (executed === totalRecords && typeof callback == 'function') {
92529 callback.call(scope || me, result, errors.length > 0 ? errors : null);
92530 }
92531 },
92532 function (transaction, error) {
92533 executed++;
92534 errors.push({
92535 clientId: id,
92536 error: error
92537 });
92538
92539 if (executed === totalRecords && typeof callback == 'function') {
92540 callback.call(scope || me, result, errors);
92541 }
92542 }
92543 );
92544 });
92545 },
92546
92547 destroyRecords: function (transaction, records, callback, scope) {
92548 var me = this,
92549 table = me.getTable(),
92550 idProperty = me.getModel().getIdProperty(),
92551 ids = [],
92552 values = [],
92553 destroyedRecords = [],
92554 i, ln, result, record;
92555
92556 for (i = 0, ln = records.length; i < ln; i++) {
92557 ids.push(idProperty + ' = ?');
92558 values.push(records[i].getId());
92559 }
92560
92561 result = new Ext.data.ResultSet({
92562 records: destroyedRecords,
92563 success: true
92564 });
92565
92566 transaction.executeSql(
92567 'DELETE FROM ' + table + ' WHERE ' + ids.join(' OR '), values,
92568 function (transaction, resultSet) {
92569 for (i = 0, ln = records.length; i < ln; i++) {
92570 record = records[i];
92571 destroyedRecords.push({
92572 id: record.getId()
92573 });
92574 }
92575
92576 if (typeof callback == 'function') {
92577 callback.call(scope || me, result);
92578 }
92579 },
92580 function (transaction, error) {
92581 if (typeof callback == 'function') {
92582 callback.call(scope || me, result, error);
92583 }
92584 }
92585 );
92586 },
92587
92588 /**
92589 * Formats the data for each record before sending it to the server. This
92590 * method should be overridden to format the data in a way that differs from the default.
92591 * @param {Object} record The record that we are writing to the server.
92592 * @return {Object} An object literal of name/value keys to be written to the server.
92593 * By default this method returns the data property on the record.
92594 */
92595 getRecordData: function (record) {
92596 var me = this,
92597 fields = record.getFields(),
92598 idProperty = record.getIdProperty(),
92599 uniqueIdStrategy = me.getUniqueIdStrategy(),
92600 data = {},
92601 name, value;
92602
92603 fields.each(function (field) {
92604 if (field.getPersist()) {
92605 name = field.getName();
92606 if (name === idProperty && !uniqueIdStrategy) {
92607 return;
92608 }
92609 value = record.get(name);
92610 if (field.getType().type == 'date') {
92611 value = me.writeDate(field, value);
92612 }
92613 data[name] = value;
92614 }
92615 }, me);
92616
92617 return data;
92618 },
92619
92620 getColumnValues: function(columns, data) {
92621 var ln = columns.length,
92622 values = [],
92623 i, column, value;
92624
92625 for (i = 0; i < ln; i++) {
92626 column = columns[i];
92627 value = data[column];
92628 if (value !== undefined) {
92629 values.push(value);
92630 }
92631 }
92632
92633 return values;
92634 },
92635
92636 getSchemaString: function() {
92637 var me = this,
92638 schema = [],
92639 model = me.getModel(),
92640 idProperty = model.getIdProperty(),
92641 fields = model.getFields().items,
92642 uniqueIdStrategy = me.getUniqueIdStrategy(),
92643 ln = fields.length,
92644 i, field, type, name;
92645
92646 for (i = 0; i < ln; i++) {
92647 field = fields[i];
92648 type = field.getType().type;
92649 name = field.getName();
92650
92651 if (name === idProperty) {
92652 if (uniqueIdStrategy) {
92653 type = me.convertToSqlType(type);
92654 schema.unshift(idProperty + ' ' + type);
92655 } else {
92656 schema.unshift(idProperty + ' INTEGER PRIMARY KEY AUTOINCREMENT');
92657 }
92658 } else {
92659 type = me.convertToSqlType(type);
92660 schema.push(name + ' ' + type);
92661 }
92662 }
92663
92664 return schema.join(', ');
92665 },
92666
92667 getPersistedModelColumns: function(model) {
92668 var fields = model.getFields().items,
92669 uniqueIdStrategy = this.getUniqueIdStrategy(),
92670 idProperty = model.getIdProperty(),
92671 columns = [],
92672 ln = fields.length,
92673 i, field, name;
92674
92675 for (i = 0; i < ln; i++) {
92676 field = fields[i];
92677 name = field.getName();
92678
92679 if (name === idProperty && !uniqueIdStrategy) {
92680 continue;
92681 }
92682
92683 if (field.getPersist()) {
92684 columns.push(field.getName());
92685 }
92686 }
92687 return columns;
92688 },
92689
92690 convertToSqlType: function(type) {
92691 switch (type.toLowerCase()) {
92692 case 'date':
92693 case 'string':
92694 case 'auto':
92695 return 'TEXT';
92696 case 'int':
92697 return 'INTEGER';
92698 case 'float':
92699 return 'REAL';
92700 case 'bool':
92701 return 'NUMERIC';
92702 }
92703 },
92704
92705 writeDate: function (field, date) {
92706 if (Ext.isEmpty(date)) {
92707 return null;
92708 }
92709
92710 var dateFormat = field.getDateFormat() || this.getDefaultDateFormat();
92711 switch (dateFormat) {
92712 case 'timestamp':
92713 return date.getTime() / 1000;
92714 case 'time':
92715 return date.getTime();
92716 default:
92717 return Ext.Date.format(date, dateFormat);
92718 }
92719 },
92720
92721 dropTable: function(config) {
92722 var me = this,
92723 table = me.getTable(),
92724 callback = config ? config.callback : null,
92725 scope = config ? config.scope || me : null,
92726 db = me.getDatabaseObject();
92727
92728 db.transaction(function(transaction) {
92729 transaction.executeSql('DROP TABLE ' + table);
92730 },
92731 function(transaction, error) {
92732 if (typeof callback == 'function') {
92733 callback.call(scope || me, false, table, error);
92734 }
92735 },
92736 function(transaction) {
92737 if (typeof callback == 'function') {
92738 callback.call(scope || me, true, table);
92739 }
92740 }
92741 );
92742
92743 me.setTableExists(false);
92744 },
92745
92746 getDatabaseObject: function() {
92747 return openDatabase(this.getDatabase(), '1.0', 'Sencha Database', 5 * 1024 * 1024);
92748 }
92749 });
92750
92751 /**
92752 * @author Ed Spencer
92753 * @class Ext.data.reader.Xml
92754 * @extends Ext.data.reader.Reader
92755 *
92756 * The XML Reader is used by a Proxy to read a server response that is sent back in XML format. This usually
92757 * happens as a result of loading a Store - for example we might create something like this:
92758 *
92759 * Ext.define('User', {
92760 * extend: 'Ext.data.Model',
92761 * config: {
92762 * fields: ['id', 'name', 'email']
92763 * }
92764 * });
92765 *
92766 * var store = Ext.create('Ext.data.Store', {
92767 * model: 'User',
92768 * proxy: {
92769 * type: 'ajax',
92770 * url : 'users.xml',
92771 * reader: {
92772 * type: 'xml',
92773 * record: 'user'
92774 * }
92775 * }
92776 * });
92777 *
92778 * The example above creates a 'User' model. Models are explained in the {@link Ext.data.Model Model} docs if you're
92779 * not already familiar with them.
92780 *
92781 * We created the simplest type of XML Reader possible by simply telling our {@link Ext.data.Store Store}'s
92782 * {@link Ext.data.proxy.Proxy Proxy} that we want a XML Reader. The Store automatically passes the configured model to the
92783 * Store, so it is as if we passed this instead:
92784 *
92785 * reader: {
92786 * type : 'xml',
92787 * model: 'User',
92788 * record: 'user'
92789 * }
92790 *
92791 * The reader we set up is ready to read data from our server - at the moment it will accept a response like this:
92792 *
92793 * <?xml version="1.0" encoding="UTF-8"?>
92794 * <users>
92795 * <user>
92796 * <id>1</id>
92797 * <name>Ed Spencer</name>
92798 * <email>ed@sencha.com</email>
92799 * </user>
92800 * <user>
92801 * <id>2</id>
92802 * <name>Abe Elias</name>
92803 * <email>abe@sencha.com</email>
92804 * </user>
92805 * </users>
92806 *
92807 * The XML Reader uses the configured {@link #record} option to pull out the data for each record - in this case we
92808 * set record to 'user', so each `<user>` above will be converted into a User model.
92809 *
92810 * ## Reading other XML formats
92811 *
92812 * If you already have your XML format defined and it doesn't look quite like what we have above, you can usually
92813 * pass XmlReader a couple of configuration options to make it parse your format. For example, we can use the
92814 * {@link #rootProperty} configuration to parse data that comes back like this:
92815 *
92816 * <?xml version="1.0" encoding="UTF-8"?>
92817 * <users>
92818 * <user>
92819 * <id>1</id>
92820 * <name>Ed Spencer</name>
92821 * <email>ed@sencha.com</email>
92822 * </user>
92823 * <user>
92824 * <id>2</id>
92825 * <name>Abe Elias</name>
92826 * <email>abe@sencha.com</email>
92827 * </user>
92828 * </users>
92829 *
92830 * To parse this we just pass in a {@link #rootProperty} configuration that matches the 'users' above:
92831 *
92832 * reader: {
92833 * type: 'xml',
92834 * record: 'user',
92835 * rootProperty: 'users'
92836 * }
92837 *
92838 * Note that XmlReader doesn't care whether your {@link #rootProperty} and {@link #record} elements are nested deep
92839 * inside a larger structure, so a response like this will still work:
92840 *
92841 * <?xml version="1.0" encoding="UTF-8"?>
92842 * <deeply>
92843 * <nested>
92844 * <xml>
92845 * <users>
92846 * <user>
92847 * <id>1</id>
92848 * <name>Ed Spencer</name>
92849 * <email>ed@sencha.com</email>
92850 * </user>
92851 * <user>
92852 * <id>2</id>
92853 * <name>Abe Elias</name>
92854 * <email>abe@sencha.com</email>
92855 * </user>
92856 * </users>
92857 * </xml>
92858 * </nested>
92859 * </deeply>
92860 *
92861 * ## Response metadata
92862 *
92863 * The server can return additional data in its response, such as the {@link #totalProperty total number of records}
92864 * and the {@link #successProperty success status of the response}. These are typically included in the XML response
92865 * like this:
92866 *
92867 * <?xml version="1.0" encoding="UTF-8"?>
92868 * <users>
92869 * <total>100</total>
92870 * <success>true</success>
92871 * <user>
92872 * <id>1</id>
92873 * <name>Ed Spencer</name>
92874 * <email>ed@sencha.com</email>
92875 * </user>
92876 * <user>
92877 * <id>2</id>
92878 * <name>Abe Elias</name>
92879 * <email>abe@sencha.com</email>
92880 * </user>
92881 * </users>
92882 *
92883 * If these properties are present in the XML response they can be parsed out by the XmlReader and used by the
92884 * Store that loaded it. We can set up the names of these properties by specifying a final pair of configuration
92885 * options:
92886 *
92887 * reader: {
92888 * type: 'xml',
92889 * rootProperty: 'users',
92890 * totalProperty : 'total',
92891 * successProperty: 'success'
92892 * }
92893 *
92894 * These final options are not necessary to make the Reader work, but can be useful when the server needs to report
92895 * an error or if it needs to indicate that there is a lot of data available of which only a subset is currently being
92896 * returned.
92897 *
92898 * ## Response format
92899 *
92900 * __Note:__ In order for the browser to parse a returned XML document, the Content-Type header in the HTTP
92901 * response must be set to "text/xml" or "application/xml". This is very important - the XmlReader will not
92902 * work correctly otherwise.
92903 */
92904 Ext.define('Ext.data.reader.Xml', {
92905 extend: Ext.data.reader.Reader ,
92906 alternateClassName: 'Ext.data.XmlReader',
92907 alias: 'reader.xml',
92908
92909 config: {
92910 /**
92911 * @cfg {String} record The DomQuery path to the repeated element which contains record information.
92912 */
92913 record: null
92914 },
92915
92916 /**
92917 * @private
92918 * Creates a function to return some particular key of data from a response. The {@link #totalProperty} and
92919 * {@link #successProperty} are treated as special cases for type casting, everything else is just a simple selector.
92920 * @param {String} expr
92921 * @return {Function}
92922 */
92923 createAccessor: function(expr) {
92924 var me = this;
92925
92926 if (Ext.isEmpty(expr)) {
92927 return Ext.emptyFn;
92928 }
92929
92930 if (Ext.isFunction(expr)) {
92931 return expr;
92932 }
92933
92934 return function(root) {
92935 return me.getNodeValue(Ext.DomQuery.selectNode(expr, root));
92936 };
92937 },
92938
92939 getNodeValue: function(node) {
92940 if (node && node.firstChild) {
92941 return node.firstChild.nodeValue;
92942 }
92943 return undefined;
92944 },
92945
92946 //inherit docs
92947 getResponseData: function(response) {
92948 // Check to see if the response is already an xml node.
92949 if (response.nodeType === 1 || response.nodeType === 9) {
92950 return response;
92951 }
92952
92953 var xml = response.responseXML;
92954
92955 //<debug>
92956 if (!xml) {
92957 /**
92958 * @event exception Fires whenever the reader is unable to parse a response.
92959 * @param {Ext.data.reader.Xml} reader A reference to this reader.
92960 * @param {XMLHttpRequest} response The XMLHttpRequest response object.
92961 * @param {String} error The error message.
92962 */
92963 this.fireEvent('exception', this, response, 'XML data not found in the response');
92964
92965 Ext.Logger.warn('XML data not found in the response');
92966 }
92967 //</debug>
92968
92969 return xml;
92970 },
92971
92972 /**
92973 * Normalizes the data object.
92974 * @param {Object} data The raw data object.
92975 * @return {Object} Returns the `documentElement` property of the data object if present, or the same object if not.
92976 */
92977 getData: function(data) {
92978 return data.documentElement || data;
92979 },
92980
92981 /**
92982 * @private
92983 * Given an XML object, returns the Element that represents the root as configured by the Reader's meta data.
92984 * @param {Object} data The XML data object.
92985 * @return {XMLElement} The root node element.
92986 */
92987 getRoot: function(data) {
92988 var nodeName = data.nodeName,
92989 root = this.getRootProperty();
92990
92991 if (!root || (nodeName && nodeName == root)) {
92992 return data;
92993 } else if (Ext.DomQuery.isXml(data)) {
92994 // This fix ensures we have XML data
92995 // Related to TreeStore calling getRoot with the root node, which isn't XML
92996 // Probably should be resolved in TreeStore at some point
92997 return Ext.DomQuery.selectNode(root, data);
92998 }
92999 },
93000
93001 /**
93002 * @private
93003 * We're just preparing the data for the superclass by pulling out the record nodes we want.
93004 * @param {XMLElement} root The XML root node.
93005 * @return {Ext.data.Model[]} The records.
93006 */
93007 extractData: function(root) {
93008 var recordName = this.getRecord();
93009
93010 //<debug>
93011 if (!recordName) {
93012 Ext.Logger.error('Record is a required parameter');
93013 }
93014 //</debug>
93015
93016 if (recordName != root.nodeName && recordName !== root.localName) {
93017 root = Ext.DomQuery.select(recordName, root);
93018 } else {
93019 root = [root];
93020 }
93021 return this.callParent([root]);
93022 },
93023
93024 /**
93025 * @private
93026 * See {@link Ext.data.reader.Reader#getAssociatedDataRoot} docs.
93027 * @param {Object} data The raw data object.
93028 * @param {String} associationName The name of the association to get data for (uses {@link Ext.data.association.Association#associationKey} if present).
93029 * @return {XMLElement} The root.
93030 */
93031 getAssociatedDataRoot: function(data, associationName) {
93032 return Ext.DomQuery.select(associationName, data)[0];
93033 },
93034
93035 /**
93036 * Parses an XML document and returns a ResultSet containing the model instances.
93037 * @param {Object} doc Parsed XML document.
93038 * @return {Ext.data.ResultSet} The parsed result set.
93039 */
93040 readRecords: function(doc) {
93041 //it's possible that we get passed an array here by associations. Make sure we strip that out (see Ext.data.reader.Reader#readAssociated)
93042 if (Ext.isArray(doc)) {
93043 doc = doc[0];
93044 }
93045 return this.callParent([doc]);
93046 },
93047
93048 /**
93049 * @private
93050 * Returns an accessor expression for the passed Field from an XML element using either the Field's mapping, or
93051 * its ordinal position in the fields collection as the index.
93052 *
93053 * This is used by `buildExtractors` to create optimized on extractor function which converts raw data into model instances.
93054 */
93055 createFieldAccessExpression: function(field, fieldVarName, dataName) {
93056 var selector = field.getMapping() || field.getName(),
93057 result;
93058
93059 if (typeof selector === 'function') {
93060 result = fieldVarName + '.getMapping()(' + dataName + ', this)';
93061 } else {
93062 selector = selector.split('@');
93063
93064 if (selector.length === 2 && selector[0]) {
93065 result = 'me.getNodeValue(Ext.DomQuery.selectNode("@' + selector[1] + '", Ext.DomQuery.selectNode("' + selector[0] + '", ' + dataName + ')))';
93066 } else if (selector.length === 2) {
93067 result = 'me.getNodeValue(Ext.DomQuery.selectNode("@' + selector[1] + '", ' + dataName + '))';
93068 } else if (selector.length === 1) {
93069 result = 'me.getNodeValue(Ext.DomQuery.selectNode("' + selector[0] + '", ' + dataName + '))';
93070 } else {
93071 throw "Unsupported query - too many queries for attributes in " + selector.join('@');
93072 }
93073 }
93074 return result;
93075 }
93076 });
93077
93078 /**
93079 * @author Ed Spencer
93080 * @class Ext.data.writer.Xml
93081 *
93082 * This class is used to write {@link Ext.data.Model} data to the server in an XML format.
93083 * The {@link #documentRoot} property is used to specify the root element in the XML document.
93084 * The {@link #record} option is used to specify the element name for each record that will make
93085 * up the XML document.
93086 */
93087 Ext.define('Ext.data.writer.Xml', {
93088
93089 /* Begin Definitions */
93090
93091 extend: Ext.data.writer.Writer ,
93092 alternateClassName: 'Ext.data.XmlWriter',
93093
93094 alias: 'writer.xml',
93095
93096 /* End Definitions */
93097
93098 config: {
93099 /**
93100 * @cfg {String} documentRoot The name of the root element of the document.
93101 * If there is more than 1 record and the root is not specified, the default document root will still be used
93102 * to ensure a valid XML document is created.
93103 */
93104 documentRoot: 'xmlData',
93105
93106 /**
93107 * @cfg {String} defaultDocumentRoot The root to be used if {@link #documentRoot} is empty and a root is required
93108 * to form a valid XML document.
93109 */
93110 defaultDocumentRoot: 'xmlData',
93111
93112 /**
93113 * @cfg {String} header A header to use in the XML document (such as setting the encoding or version).
93114 */
93115 header: '',
93116
93117 /**
93118 * @cfg {String} record The name of the node to use for each record.
93119 */
93120 record: 'record'
93121 },
93122
93123 /**
93124 * @param {Object} request
93125 * @param {Array} data
93126 * @return {Object}
93127 */
93128 writeRecords: function(request, data) {
93129 var me = this,
93130 xml = [],
93131 i = 0,
93132 len = data.length,
93133 root = me.getDocumentRoot(),
93134 record = me.getRecord(),
93135 needsRoot = data.length !== 1,
93136 item,
93137 key;
93138
93139 // may not exist
93140 xml.push(me.getHeader() || '');
93141
93142 if (!root && needsRoot) {
93143 root = me.getDefaultDocumentRoot();
93144 }
93145
93146 if (root) {
93147 xml.push('<', root, '>');
93148 }
93149
93150 for (; i < len; ++i) {
93151 item = data[i];
93152 xml.push('<', record, '>');
93153 for (key in item) {
93154 if (item.hasOwnProperty(key)) {
93155 xml.push('<', key, '>', item[key], '</', key, '>');
93156 }
93157 }
93158 xml.push('</', record, '>');
93159 }
93160
93161 if (root) {
93162 xml.push('</', root, '>');
93163 }
93164
93165 request.setXmlData(xml.join(''));
93166 return request;
93167 }
93168 });
93169
93170 /**
93171 * IndexBar is a component used to display a list of data (primarily an alphabet) which can then be used to quickly
93172 * navigate through a list (see {@link Ext.List}) of data. When a user taps on an item in the {@link Ext.IndexBar},
93173 * it will fire the {@link #index} event.
93174 *
93175 * Here is an example of the usage in a {@link Ext.List}:
93176 *
93177 * @example phone portrait preview
93178 * Ext.define('Contact', {
93179 * extend: 'Ext.data.Model',
93180 * config: {
93181 * fields: ['firstName', 'lastName']
93182 * }
93183 * });
93184 *
93185 * var store = new Ext.data.JsonStore({
93186 * model: 'Contact',
93187 * sorters: 'lastName',
93188 *
93189 * grouper: {
93190 * groupFn: function(record) {
93191 * return record.get('lastName')[0];
93192 * }
93193 * },
93194 *
93195 * data: [
93196 * {firstName: 'Tommy', lastName: 'Maintz'},
93197 * {firstName: 'Rob', lastName: 'Dougan'},
93198 * {firstName: 'Ed', lastName: 'Spencer'},
93199 * {firstName: 'Jamie', lastName: 'Avins'},
93200 * {firstName: 'Aaron', lastName: 'Conran'},
93201 * {firstName: 'Dave', lastName: 'Kaneda'},
93202 * {firstName: 'Jacky', lastName: 'Nguyen'},
93203 * {firstName: 'Abraham', lastName: 'Elias'},
93204 * {firstName: 'Jay', lastName: 'Robinson'},
93205 * {firstName: 'Nigel', lastName: 'White'},
93206 * {firstName: 'Don', lastName: 'Griffin'},
93207 * {firstName: 'Nico', lastName: 'Ferrero'},
93208 * {firstName: 'Jason', lastName: 'Johnston'}
93209 * ]
93210 * });
93211 *
93212 * var list = new Ext.List({
93213 * fullscreen: true,
93214 * itemTpl: '<div class="contact">{firstName} <strong>{lastName}</strong></div>',
93215 *
93216 * grouped : true,
93217 * indexBar : true,
93218 * store: store,
93219 * hideOnMaskTap: false
93220 * });
93221 *
93222 * ###Further Reading
93223 * [Sencha Touch DataView Guide](../../../components/list.html)
93224 */
93225 Ext.define('Ext.dataview.IndexBar', {
93226 extend: Ext.Component ,
93227 alternateClassName: 'Ext.IndexBar',
93228
93229 /**
93230 * @event index
93231 * Fires when an item in the index bar display has been tapped.
93232 * @param {Ext.dataview.IndexBar} this The IndexBar instance
93233 * @param {String} html The HTML inside the tapped node.
93234 * @param {Ext.dom.Element} target The node on the indexbar that has been tapped.
93235 */
93236
93237 config: {
93238 baseCls: Ext.baseCSSPrefix + 'indexbar',
93239
93240 /**
93241 * @cfg {String} direction
93242 * Layout direction, can be either 'vertical' or 'horizontal'
93243 * @accessor
93244 */
93245 direction: 'vertical',
93246
93247 /**
93248 * @cfg {Array} letters
93249 * The letters to show on the index bar.
93250 * @accessor
93251 */
93252 letters: ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'],
93253
93254 ui: 'alphabet',
93255
93256 /**
93257 * @cfg {String} listPrefix
93258 * The prefix string to be used at the beginning of the list.
93259 * E.g: useful to add a "#" prefix before numbers.
93260 * @accessor
93261 */
93262 listPrefix: null
93263 },
93264 platformConfig: [
93265 {
93266 theme: ['Blackberry', 'Blackberry103'],
93267 direction: 'vertical',
93268 letters: ['*', '#', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']
93269 }
93270 ],
93271 // @private
93272 itemCls: Ext.baseCSSPrefix + '',
93273
93274 updateDirection: function(newDirection, oldDirection) {
93275 var baseCls = this.getBaseCls();
93276
93277 this.element.replaceCls(baseCls + '-' + oldDirection, baseCls + '-' + newDirection);
93278 },
93279
93280 getElementConfig: function() {
93281 // Blackberry Specific code for Index Bar Indicator
93282 if (Ext.theme.is.Blackberry || Ext.theme.is.Blackberry103) {
93283 return {
93284 reference: 'wrapper',
93285 classList: ['x-centered', 'x-indexbar-wrapper'],
93286 children: [
93287 {
93288 reference: 'indicator',
93289 classList: ['x-indexbar-indicator'],
93290 hidden: true,
93291 children: [{
93292 reference: 'indicatorInner',
93293 classList: ['x-indexbar-indicator-inner']
93294 }]
93295 },
93296 this.callParent()
93297 ]
93298 };
93299 } else {
93300 return {
93301 reference: 'wrapper',
93302 classList: ['x-centered', 'x-indexbar-wrapper'],
93303 children: [this.callParent()]
93304 };
93305 }
93306 },
93307
93308 updateLetters: function(letters) {
93309 this.innerElement.setHtml('');
93310
93311 if (letters) {
93312 var ln = letters.length,
93313 i;
93314
93315 for (i = 0; i < ln; i++) {
93316 this.innerElement.createChild({
93317 html: letters[i]
93318 });
93319 }
93320 }
93321 },
93322
93323 updateListPrefix: function(listPrefix) {
93324 if (listPrefix && listPrefix.length) {
93325 this.innerElement.createChild({
93326 html: listPrefix
93327 }, 0);
93328 }
93329 },
93330
93331 // @private
93332 initialize: function() {
93333 this.callParent();
93334
93335 this.innerElement.on({
93336 touchstart: this.onTouchStart,
93337 touchend: this.onTouchEnd,
93338 dragend: this.onDragEnd,
93339 drag: this.onDrag,
93340 scope: this
93341 });
93342 },
93343
93344 onTouchStart: function(e) {
93345 e.stopPropagation();
93346 this.innerElement.addCls(this.getBaseCls() + '-pressed');
93347 this.pageBox = this.innerElement.getPageBox();
93348 this.onDrag(e);
93349 },
93350
93351 onTouchEnd: function(e) {
93352 this.onDragEnd();
93353 },
93354
93355 // @private
93356 onDragEnd: function() {
93357 this.innerElement.removeCls(this.getBaseCls() + '-pressed');
93358
93359 // Blackberry Specific code for Index Bar Indicator
93360 if(this.indicator) {
93361 this.indicator.hide();
93362 }
93363 },
93364
93365 // @private
93366 onDrag: function(e) {
93367 var point = Ext.util.Point.fromEvent(e),
93368 target, isValidTarget,
93369 pageBox = this.pageBox;
93370
93371 if (!pageBox) {
93372 pageBox = this.pageBox = this.el.getPageBox();
93373 }
93374
93375
93376 if (this.getDirection() === 'vertical') {
93377 if (point.y > pageBox.bottom || point.y < pageBox.top) {
93378 return;
93379 }
93380 target = Ext.Element.fromPoint(pageBox.left + (pageBox.width / 2), point.y);
93381 isValidTarget = target.getParent() == this.element;
93382
93383 // Blackberry Specific code for Index Bar Indicator
93384 if(this.indicator) {
93385 this.indicator.show();
93386
93387 var halfIndicatorHeight = this.indicator.getHeight() / 2,
93388 y = point.y - this.element.getY();
93389
93390 y = Math.min(Math.max(y, halfIndicatorHeight), this.element.getHeight() - halfIndicatorHeight);
93391
93392 if (this.indicatorInner && isValidTarget) {
93393 this.indicatorInner.setHtml(target.getHtml().toUpperCase());
93394 }
93395 this.indicator.setTop(y - (halfIndicatorHeight));
93396 }
93397 }
93398 else {
93399 if (point.x > pageBox.right || point.x < pageBox.left) {
93400 return;
93401 }
93402 target = Ext.Element.fromPoint(point.x, pageBox.top + (pageBox.height / 2));
93403 isValidTarget = target.getParent() == this.element;
93404 }
93405
93406 if (target && isValidTarget) {
93407 this.fireEvent('index', this, target.dom.innerHTML, target);
93408 }
93409 },
93410
93411 destroy: function() {
93412 var me = this,
93413 elements = Array.prototype.slice.call(me.innerElement.dom.childNodes),
93414 ln = elements.length,
93415 i = 0;
93416
93417 for (; i < ln; i++) {
93418 Ext.removeNode(elements[i]);
93419 }
93420 this.callParent();
93421 }
93422
93423 }, function() {
93424 });
93425
93426 /**
93427 * @private - To be made a sample
93428 */
93429 Ext.define('Ext.dataview.ListItemHeader', {
93430 extend: Ext.Component ,
93431 xtype : 'listitemheader',
93432
93433 config: {
93434 /**
93435 * @cfg
93436 * @inheritdoc
93437 */
93438 baseCls: Ext.baseCSSPrefix + 'list-header'
93439 }
93440 });
93441
93442 /**
93443 * A ListItem is a container for {@link Ext.dataview.List} with
93444 * useSimpleItems: false.
93445 *
93446 * ListItem configures and updates the {@link Ext.data.Model records} for
93447 * the sub-component items in a list.
93448 *
93449 * Overwrite the `updateRecord()` method to set a sub-component's value.
93450 * Sencha Touch calls `updateRecord()` whenever the data in the list updates.
93451 *
93452 * The `updatedata` event fires after `updateRecord()` runs.
93453 *
93454 * *Note*: Use of ListItem increases overhead since it generates more markup than
93455 * using the List class with useSimpleItems: true. This overhead is more
93456 * noticeable in Internet Explorer. If at all possible, use
93457 * {@link Ext.dataview.component.SimpleListItem} instead.
93458 *
93459 * The following example shows how to configure and update sub-component items
93460 * in a list:
93461 *
93462 * Ext.define('Twitter.view.TweetListItem', {
93463 * extend: 'Ext.dataview.component.ListItem',
93464 * xtype : 'tweetlistitem',
93465 * requires: [
93466 * 'Ext.Img'
93467 * ],
93468 * config: {
93469 * userName: {
93470 * cls: 'username'
93471 * },
93472 * text: {
93473 * cls: 'text'
93474 * },
93475 * avatar: {
93476 * docked: 'left',
93477 * xtype : 'image',
93478 * cls : 'avatar',
93479 * width: '48px',
93480 * height: '48px'
93481 * },
93482 * layout: {
93483 * type: 'vbox'
93484 * }
93485 * },
93486 *
93487 * applyUserName: function(config) {
93488 * return Ext.factory(config, Ext.Component, this.getUserName());
93489 * },
93490 *
93491 * updateUserName: function(newUserName) {
93492 * if (newUserName) {
93493 * this.insert(0, newUserName);
93494 * }
93495 * },
93496 *
93497 * applyText: function(config) {
93498 * return Ext.factory(config, Twitter.view.TweetListItemText, this.getText());
93499 * },
93500 *
93501 * updateText: function(newText) {
93502 * if (newText) {
93503 * this.add(newText);
93504 * }
93505 * },
93506 *
93507 * applyAvatar: function(config) {
93508 * return Ext.factory(config, Ext.Img, this.getAvatar());
93509 * },
93510 *
93511 * updateAvatar: function(newAvatar) {
93512 * if (newAvatar) {
93513 * this.add(newAvatar);
93514 * }
93515 * },
93516 *
93517 * updateRecord: function(record) {
93518 * if (!record) {
93519 * return;
93520 * }
93521 *
93522 * this.getUserName().setHtml(record.get('username'));
93523 * this.getText().setHtml(record.get('text'));
93524 * this.getAvatar().setSrc(record.get('avatar_url'));
93525 * this.callParent(arguments);
93526 *
93527 * }
93528 * });
93529 *
93530 */
93531 Ext.define('Ext.dataview.component.ListItem', {
93532 extend: Ext.dataview.component.DataItem ,
93533 xtype : 'listitem',
93534
93535 config: {
93536 baseCls: Ext.baseCSSPrefix + 'list-item',
93537
93538 dataMap: null,
93539
93540 body: {
93541 xtype: 'component',
93542 cls: 'x-list-item-body'
93543 },
93544
93545 disclosure: {
93546 xtype: 'component',
93547 cls: 'x-list-disclosure',
93548 hidden: true,
93549 docked: 'right'
93550 },
93551
93552 header: {
93553 xtype: 'component',
93554 cls: 'x-list-header',
93555 html: ' '
93556 },
93557
93558 tpl: null,
93559 items: null
93560 },
93561
93562 applyBody: function(body) {
93563 if (body && !body.isComponent) {
93564 body = Ext.factory(body, Ext.Component, this.getBody());
93565 }
93566 return body;
93567 },
93568
93569 updateBody: function(body, oldBody) {
93570 if (body) {
93571 this.add(body);
93572 } else if (oldBody) {
93573 oldBody.destroy();
93574 }
93575 },
93576
93577 applyHeader: function(header) {
93578 if (header && !header.isComponent) {
93579 header = Ext.factory(header, Ext.Component, this.getHeader());
93580 }
93581 return header;
93582 },
93583
93584 updateHeader: function(header, oldHeader) {
93585 if (oldHeader) {
93586 oldHeader.destroy();
93587 }
93588 },
93589
93590 applyDisclosure: function(disclosure) {
93591 if (disclosure && !disclosure.isComponent) {
93592 disclosure = Ext.factory(disclosure, Ext.Component, this.getDisclosure());
93593 }
93594 return disclosure;
93595 },
93596
93597 updateDisclosure: function(disclosure, oldDisclosure) {
93598 if (disclosure) {
93599 this.add(disclosure);
93600 } else if (oldDisclosure) {
93601 oldDisclosure.destroy();
93602 }
93603 },
93604
93605 updateTpl: function(tpl) {
93606 this.getBody().setTpl(tpl);
93607 },
93608
93609 updateRecord: function(record) {
93610 var me = this,
93611 dataview = me.dataview || this.getDataview(),
93612 data = record && dataview.prepareData(record.getData(true), dataview.getStore().indexOf(record), record),
93613 dataMap = me.getDataMap(),
93614 body = this.getBody(),
93615 disclosure = this.getDisclosure();
93616
93617 me._record = record;
93618
93619 if (dataMap) {
93620 me.doMapData(dataMap, data, body);
93621 } else if (body) {
93622 body.updateData(data || null);
93623 }
93624
93625 if (disclosure && record && dataview.getOnItemDisclosure()) {
93626 var disclosureProperty = dataview.getDisclosureProperty();
93627 disclosure[(data.hasOwnProperty(disclosureProperty) && data[disclosureProperty] === false) ? 'hide' : 'show']();
93628 }
93629
93630 /**
93631 * @event updatedata
93632 * Fires whenever the data of the DataItem is updated.
93633 * @param {Ext.dataview.component.DataItem} this The DataItem instance.
93634 * @param {Object} newData The new data.
93635 */
93636 me.fireEvent('updatedata', me, data);
93637 },
93638
93639 destroy: function() {
93640 Ext.destroy(this.getHeader());
93641 this.callParent(arguments);
93642 }
93643 });
93644
93645 /**
93646 * A SimpleListItem is a simplified list item that is used by {@link Ext.dataview.List} when
93647 * useSimpleItems is set to true. It supports disclosure icons and headers and generates the
93648 * slimmest markup possible to achieve this. It doesn't support container functionality like adding
93649 * or docking items. If you require those features you should have your list use
93650 * {@link Ext.dataview.component.ListItem} instances.
93651 */
93652 Ext.define('Ext.dataview.component.SimpleListItem', {
93653 extend: Ext.Component ,
93654 xtype : 'simplelistitem',
93655
93656 config: {
93657 baseCls: Ext.baseCSSPrefix + 'list-item',
93658
93659 disclosure: {
93660 xtype: 'component',
93661 cls: 'x-list-disclosure',
93662 hidden: true
93663 },
93664
93665 header: {
93666 xtype: 'component',
93667 cls: 'x-list-header',
93668 html: ' '
93669 },
93670
93671 /*
93672 * @private dataview
93673 */
93674 dataview: null,
93675
93676 /**
93677 * @cfg {Ext.data.Model} record The model instance of this ListTplItem. It is controlled by the List.
93678 * @accessor
93679 */
93680 record: null
93681 },
93682
93683 initialize: function() {
93684 this.element.addCls(this.getBaseCls() + '-tpl');
93685 },
93686
93687 applyHeader: function(header) {
93688 if (header && !header.isComponent) {
93689 header = Ext.factory(header, Ext.Component, this.getHeader());
93690 }
93691 return header;
93692 },
93693
93694 updateHeader: function(header, oldHeader) {
93695 if (oldHeader) {
93696 oldHeader.destroy();
93697 }
93698 },
93699
93700 applyDisclosure: function(disclosure) {
93701 if (disclosure && !disclosure.isComponent) {
93702 disclosure = Ext.factory(disclosure, Ext.Component, this.getDisclosure());
93703 }
93704 return disclosure;
93705 },
93706
93707 updateDisclosure: function(disclosure, oldDisclosure) {
93708 if (disclosure) {
93709 this.element.appendChild(disclosure.renderElement);
93710 } else if (oldDisclosure) {
93711 oldDisclosure.destroy();
93712 }
93713 },
93714
93715 updateRecord: function(record) {
93716 var me = this,
93717 dataview = me.dataview || this.getDataview(),
93718 data = record && dataview.prepareData(record.getData(true), dataview.getStore().indexOf(record), record),
93719 disclosure = this.getDisclosure();
93720
93721 me.updateData(data || null);
93722
93723 if (disclosure && record && dataview.getOnItemDisclosure()) {
93724 var disclosureProperty = dataview.getDisclosureProperty();
93725 disclosure[(data.hasOwnProperty(disclosureProperty) && data[disclosureProperty] === false) ? 'hide' : 'show']();
93726 }
93727 },
93728
93729 destroy: function() {
93730 Ext.destroy(this.getHeader(), this.getDisclosure());
93731 this.callParent(arguments);
93732 }
93733 });
93734
93735 /**
93736 * @private
93737 */
93738 Ext.define('Ext.util.PositionMap', {
93739 config: {
93740 minimumHeight: 50
93741 },
93742
93743 constructor: function(config) {
93744 this.map = [];
93745 this.adjustments = {};
93746 this.offset = 0;
93747
93748 this.initConfig(config);
93749 },
93750
93751 populate: function(count, offset) {
93752 var map = this.map = this.map || [],
93753 minimumHeight = this.getMinimumHeight(),
93754 i, previousIndex, ln;
93755
93756 offset = offset || 0;
93757
93758 // We add 1 item to the count so that we can get the height of the bottom item
93759 count++;
93760 map.length = count;
93761
93762 map[0] = 0;
93763 for (i = offset + 1, ln = count - 1; i <= ln; i++) {
93764 previousIndex = i - 1;
93765 map[i] = map[previousIndex] + minimumHeight;
93766 }
93767
93768 this.adjustments = {
93769 indices: [],
93770 heights: {}
93771 };
93772 this.offset = 0;
93773 for (i = 1, ln = count - 1; i <= ln; i++) {
93774 previousIndex = i - 1;
93775 this.offset += map[i] - map[previousIndex] - minimumHeight;
93776 }
93777 },
93778
93779 setItemHeight: function(index, height) {
93780 height = Math.max(height, this.getMinimumHeight());
93781 if (height !== this.getItemHeight(index)) {
93782 var adjustments = this.adjustments;
93783 adjustments.indices.push(parseInt(index, 10));
93784 adjustments.heights[index] = height;
93785 }
93786 },
93787
93788 update: function() {
93789 var adjustments = this.adjustments,
93790 indices = adjustments.indices,
93791 heights = adjustments.heights,
93792 map = this.map,
93793 ln = indices.length,
93794 minimumHeight = this.getMinimumHeight(),
93795 difference = 0,
93796 i, j, height, index, nextIndex, currentHeight;
93797
93798 if (!adjustments.indices.length) {
93799 return false;
93800 }
93801
93802 Ext.Array.sort(indices, function(a, b) {
93803 return a - b;
93804 });
93805
93806 for (i = 0; i < ln; i++) {
93807 index = indices[i];
93808 nextIndex = indices[i + 1] || map.length - 1;
93809
93810 currentHeight = (map[index + 1] !== undefined) ? (map[index + 1] - map[index] + difference) : minimumHeight;
93811 height = heights[index];
93812
93813 difference += height - currentHeight;
93814
93815 for (j = index + 1; j <= nextIndex; j++) {
93816 map[j] += difference;
93817 }
93818 }
93819
93820 this.offset += difference;
93821 this.adjustments = {
93822 indices: [],
93823 heights: {}
93824 };
93825 return true;
93826 },
93827
93828 getItemHeight: function(index) {
93829 return this.map[index + 1] - this.map[index];
93830 },
93831
93832 getTotalHeight: function() {
93833 return ((this.map.length - 1) * this.getMinimumHeight()) + this.offset;
93834 },
93835
93836 findIndex: function(pos) {
93837 return this.map.length ? this.binarySearch(this.map, pos) : 0;
93838 },
93839
93840 binarySearch: function(sorted, value) {
93841 var start = 0,
93842 end = sorted.length;
93843
93844 if (value < sorted[0]) {
93845 return 0;
93846 }
93847 if (value > sorted[end - 1]) {
93848 return end - 1;
93849 }
93850 while (start + 1 < end) {
93851 var mid = (start + end) >> 1,
93852 val = sorted[mid];
93853 if (val == value) {
93854 return mid;
93855 } else if (val < value) {
93856 start = mid;
93857 } else {
93858 end = mid;
93859 }
93860 }
93861 return start;
93862 }
93863 });
93864
93865 /**
93866 * List is a custom styled DataView which allows Grouping, Indexing, Icons, and a Disclosure. See the
93867 * [Guide](../../../components/list.html) for more.
93868 *
93869 * @example miniphone preview
93870 * Ext.create('Ext.List', {
93871 * fullscreen: true,
93872 * itemTpl: '{title}',
93873 * data: [
93874 * { title: 'Item 1' },
93875 * { title: 'Item 2' },
93876 * { title: 'Item 3' },
93877 * { title: 'Item 4' }
93878 * ]
93879 * });
93880 *
93881 * A more advanced example showing a list of people grouped by last name:
93882 *
93883 * @example miniphone preview
93884 * Ext.define('Contact', {
93885 * extend: 'Ext.data.Model',
93886 * config: {
93887 * fields: ['firstName', 'lastName']
93888 * }
93889 * });
93890 *
93891 * var store = Ext.create('Ext.data.Store', {
93892 * model: 'Contact',
93893 * sorters: 'lastName',
93894 *
93895 * grouper: {
93896 * groupFn: function(record) {
93897 * return record.get('lastName')[0];
93898 * }
93899 * },
93900 *
93901 * data: [
93902 * { firstName: 'Tommy', lastName: 'Maintz' },
93903 * { firstName: 'Rob', lastName: 'Dougan' },
93904 * { firstName: 'Ed', lastName: 'Spencer' },
93905 * { firstName: 'Jamie', lastName: 'Avins' },
93906 * { firstName: 'Aaron', lastName: 'Conran' },
93907 * { firstName: 'Dave', lastName: 'Kaneda' },
93908 * { firstName: 'Jacky', lastName: 'Nguyen' },
93909 * { firstName: 'Abraham', lastName: 'Elias' },
93910 * { firstName: 'Jay', lastName: 'Robinson'},
93911 * { firstName: 'Nigel', lastName: 'White' },
93912 * { firstName: 'Don', lastName: 'Griffin' },
93913 * { firstName: 'Nico', lastName: 'Ferrero' },
93914 * { firstName: 'Jason', lastName: 'Johnston'}
93915 * ]
93916 * });
93917 *
93918 * Ext.create('Ext.List', {
93919 * fullscreen: true,
93920 * itemTpl: '<div class="contact">{firstName} <strong>{lastName}</strong></div>',
93921 * store: store,
93922 * grouped: true
93923 * });
93924 *
93925 * If you want to dock items to the bottom or top of a List, you can use the scrollDock configuration on child items in this List. The following example adds a button to the bottom of the List.
93926 *
93927 * @example phone preview
93928 * Ext.define('Contact', {
93929 * extend: 'Ext.data.Model',
93930 * config: {
93931 * fields: ['firstName', 'lastName']
93932 * }
93933 * });
93934 *
93935 * var store = Ext.create('Ext.data.Store', {
93936 * model: 'Contact',
93937 * sorters: 'lastName',
93938 *
93939 * grouper: {
93940 * groupFn: function(record) {
93941 * return record.get('lastName')[0];
93942 * }
93943 * },
93944 *
93945 * data: [
93946 * { firstName: 'Tommy', lastName: 'Maintz' },
93947 * { firstName: 'Rob', lastName: 'Dougan' },
93948 * { firstName: 'Ed', lastName: 'Spencer' },
93949 * { firstName: 'Jamie', lastName: 'Avins' },
93950 * { firstName: 'Aaron', lastName: 'Conran' },
93951 * { firstName: 'Dave', lastName: 'Kaneda' },
93952 * { firstName: 'Jacky', lastName: 'Nguyen' },
93953 * { firstName: 'Abraham', lastName: 'Elias' },
93954 * { firstName: 'Jay', lastName: 'Robinson'},
93955 * { firstName: 'Nigel', lastName: 'White' },
93956 * { firstName: 'Don', lastName: 'Griffin' },
93957 * { firstName: 'Nico', lastName: 'Ferrero' },
93958 * { firstName: 'Jason', lastName: 'Johnston'}
93959 * ]
93960 * });
93961 *
93962 * Ext.create('Ext.List', {
93963 * fullscreen: true,
93964 * itemTpl: '<div class="contact">{firstName} <strong>{lastName}</strong></div>',
93965 * store: store,
93966 * items: [{
93967 * xtype: 'button',
93968 * scrollDock: 'bottom',
93969 * docked: 'bottom',
93970 * text: 'Load More...'
93971 * }]
93972 * });
93973 *
93974 * ###Further Reading
93975 * [Sencha Touch DataView Guide](../../../components/list.html)
93976 */
93977 Ext.define('Ext.dataview.List', {
93978 alternateClassName: 'Ext.List',
93979 extend: Ext.dataview.DataView ,
93980 xtype: 'list',
93981
93982 mixins: [ Ext.mixin.Bindable ],
93983
93984
93985
93986
93987
93988
93989
93990
93991
93992
93993 /**
93994 * @event disclose
93995 * @preventable doDisclose
93996 * Fires whenever a disclosure is handled
93997 * @param {Ext.dataview.List} this The List instance
93998 * @param {Ext.data.Model} record The record associated to the item
93999 * @param {HTMLElement} target The element disclosed
94000 * @param {Number} index The index of the item disclosed
94001 * @param {Ext.EventObject} e The event object
94002 */
94003
94004 config: {
94005 /**
94006 * @cfg layout
94007 * Hide layout config in DataView. It only causes confusion.
94008 * @accessor
94009 * @private
94010 */
94011 layout: 'fit',
94012
94013 /**
94014 * @cfg {Boolean/Object} indexBar
94015 * `true` to render an alphabet IndexBar docked on the right.
94016 * This can also be a config object that will be passed to {@link Ext.IndexBar}.
94017 * @accessor
94018 */
94019 indexBar: false,
94020
94021 icon: null,
94022
94023 /**
94024 * @cfg {Boolean} preventSelectionOnDisclose `true` to prevent the item selection when the user
94025 * taps a disclose icon.
94026 * @accessor
94027 */
94028 preventSelectionOnDisclose: true,
94029
94030 /**
94031 * @cfg baseCls
94032 * @inheritdoc
94033 */
94034 baseCls: Ext.baseCSSPrefix + 'list',
94035
94036 /**
94037 * @cfg {Boolean} pinHeaders
94038 * Whether or not to pin headers on top of item groups while scrolling for an iPhone native list experience.
94039 * @accessor
94040 */
94041 pinHeaders: true,
94042
94043 /**
94044 * @cfg {Boolean} grouped
94045 * Whether or not to group items in the provided Store with a header for each item.
94046 * @accessor
94047 */
94048 grouped: null,
94049
94050 /**
94051 * @cfg {Boolean/Function/Object} onItemDisclosure
94052 * `true` to display a disclosure icon on each list item.
94053 * The list will still fire the disclose event, and the event can be stopped before itemtap.
94054 * By setting this config to a function, the function passed will be called when the disclosure
94055 * is tapped.
94056 * Finally you can specify an object with a 'scope' and 'handler'
94057 * property defined. This will also be bound to the tap event listener
94058 * and is useful when you want to change the scope of the handler.
94059 * @accessor
94060 */
94061 onItemDisclosure: null,
94062
94063 /**
94064 * @cfg {String} disclosureProperty
94065 * A property to check on each record to display the disclosure on a per record basis. This
94066 * property must be false to prevent the disclosure from being displayed on the item.
94067 * @accessor
94068 */
94069 disclosureProperty: 'disclosure',
94070
94071 /**
94072 * @cfg {String} ui
94073 * The style of this list. Available options are `normal` and `round`.
94074 * Please note: if you use the `round` UI, {@link #pinHeaders} will be automatically turned off as
94075 * it is not supported.
94076 */
94077 ui: 'normal',
94078
94079 /**
94080 * @cfg {Boolean} useComponents
94081 * Flag the use a component based DataView implementation. This allows the full use of components in the
94082 * DataView at the cost of some performance.
94083 *
94084 * Checkout the [DataView Guide](#!/guide/dataview) for more information on using this configuration.
94085 * @accessor
94086 * @private
94087 */
94088
94089 /**
94090 * @cfg {Object} itemConfig
94091 * A configuration object that is passed to every item created by a component based DataView. Because each
94092 * item that a List renders is a Component, we can pass configuration options to each component to
94093 * easily customize how each child component behaves.
94094 * @accessor
94095 * @private
94096 */
94097
94098 /**
94099 * @cfg {Number} maxItemCache
94100 * Maintains a cache of reusable components when using a component based DataView. Improving performance at
94101 * the cost of memory.
94102 * Note this is currently only used when useComponents is true.
94103 * @accessor
94104 * @private
94105 */
94106
94107 /**
94108 * @cfg {String} defaultType
94109 * The xtype used for the component based DataView. Defaults to dataitem.
94110 * Note this is only used when useComponents is true.
94111 * @accessor
94112 */
94113 defaultType: undefined,
94114
94115 /**
94116 * @cfg {Object} itemMap
94117 * @private
94118 */
94119 itemMap: {},
94120
94121 /**
94122 * @cfg {Number} itemHeight
94123 * This allows you to set the default item height and is used to roughly calculate the amount
94124 * of items needed to fill the list. By default items are around 50px high.
94125 */
94126 itemHeight: 42,
94127
94128 /**
94129 * @cfg {Boolean} variableHeights
94130 * This configuration allows you optimize the list by not having it read the DOM heights of list items.
94131 * Instead it will assume (and set) the height to be the {@link #itemHeight}.
94132 */
94133 variableHeights: false,
94134
94135 /**
94136 * @cfg {Boolean} refreshHeightOnUpdate
94137 * Set this to false if you make many updates to your list (like in an interval), but updates
94138 * won't affect the item's height. Doing this will increase the performance of these updates.
94139 */
94140 refreshHeightOnUpdate: true,
94141
94142 /**
94143 * @cfg {Boolean} infinite
94144 * Set this to false to render all items in this list, and render them relatively.
94145 * Note that this configuration can not be dynamically changed after the list has instantiated.
94146 */
94147 infinite: false,
94148
94149 /**
94150 * @cfg {Boolean} useSimpleItems
94151 * Set this to true if you just want to have the list create simple items that use the itemTpl.
94152 * These simple items still support headers, grouping and disclosure functionality but avoid
94153 * container layouts and deeply nested markup. For many Lists using this configuration will
94154 * drastically increase the scrolling and render performance.
94155 */
94156 useSimpleItems: true,
94157
94158 /**
94159 * @cfg {Object} scrollable
94160 * @private
94161 */
94162 scrollable: null,
94163
94164 /**
94165 * The amount of items we render additionaly besides the ones currently visible.
94166 * We try to prevent the rendering of items while scrolling until the next time you stop scrolling.
94167 * If you scroll close to the end of the buffer, we start rendering individual items to always
94168 * have the {@link #minimumBufferSize} prepared.
94169 * @type {Number}
94170 */
94171 bufferSize: 20,
94172
94173 minimumBufferDistance: 5,
94174
94175 /**
94176 * @cfg {Boolean} striped
94177 * Set this to true if you want the items in the list to be zebra striped, alternating their
94178 * background color.
94179 */
94180 striped: false
94181 },
94182
94183 platformConfig: [
94184 {
94185 theme: ['Windows'],
94186 itemHeight: 44
94187 },
94188 {
94189 theme: ['Cupertino'],
94190 itemHeight: 43
94191 }
94192 ],
94193
94194 topRenderedIndex: 0,
94195 topVisibleIndex: 0,
94196 visibleCount: 0,
94197
94198 constructor: function() {
94199 var me = this, layout;
94200
94201 me.callParent(arguments);
94202
94203 //<debug>
94204 layout = this.getLayout();
94205 if (layout && !layout.isFit) {
94206 Ext.Logger.error('The base layout for a DataView must always be a Fit Layout');
94207 }
94208 //</debug>
94209 },
94210
94211 // We create complex instance arrays and objects in beforeInitialize so that we can use these inside of the initConfig process.
94212 beforeInitialize: function() {
94213 var me = this,
94214 container = me.container,
94215 baseCls = me.getBaseCls(),
94216 scrollable, scrollViewElement, pinnedHeader;
94217
94218 Ext.apply(me, {
94219 listItems: [],
94220 headerItems: [],
94221 updatedItems: [],
94222 headerMap: [],
94223 scrollDockItems: {
94224 top: [],
94225 bottom: []
94226 }
94227 });
94228
94229 // We determine the translation methods for headers and items within this List based
94230 // on the best strategy for the device
94231 this.translationMethod = Ext.browser.is.AndroidStock2 ? 'cssposition' : 'csstransform';
94232
94233 // Create the inner container that will actually hold all the list items
94234 if (!container) {
94235 container = me.container = Ext.factory({
94236 xtype: 'container',
94237 scrollable: {
94238 scroller: {
94239 autoRefresh: !me.getInfinite(),
94240 direction: 'vertical'
94241 }
94242 }
94243 });
94244 }
94245
94246 // We add the container after creating it manually because when you add the container,
94247 // the items config is initialized. When this happens, any scrollDock items will be added,
94248 // which in turn tries to add these items to the container
94249 me.add(container);
94250
94251 // We make this List's scrollable the inner containers scrollable
94252 scrollable = container.getScrollable();
94253 scrollViewElement = me.scrollViewElement = scrollable.getElement();
94254 me.scrollElement = scrollable.getScroller().getElement();
94255
94256 me.setScrollable(scrollable);
94257 me.scrollableBehavior = container.getScrollableBehavior();
94258
94259 // Create the pinnedHeader instance thats being used when grouping is enabled
94260 // and insert it into the scrollElement
94261 pinnedHeader = me.pinnedHeader = Ext.factory({
94262 xtype: 'listitemheader',
94263 html: '&nbsp;',
94264 translatable: {
94265 translationMethod: this.translationMethod
94266 },
94267 cls: [baseCls + '-header', baseCls + '-header-swap']
94268 });
94269 pinnedHeader.translate(0, -10000);
94270 pinnedHeader.$position = -10000;
94271 scrollViewElement.insertFirst(pinnedHeader.renderElement);
94272
94273 // We want to intercept any translate calls made on the scroller to perform specific list logic
94274 me.bind(scrollable.getScroller().getTranslatable(), 'doTranslate', 'onTranslate');
94275 },
94276
94277 // We override DataView's initialize method with an empty function
94278 initialize: function() {
94279 var me = this,
94280 container = me.container,
94281 scrollViewElement = me.scrollViewElement,
94282 indexBar = me.getIndexBar(),
94283 triggerEvent = me.getTriggerEvent(),
94284 triggerCtEvent = me.getTriggerCtEvent();
94285
94286 if (indexBar) {
94287 scrollViewElement.appendChild(indexBar.renderElement);
94288 }
94289
94290 if (triggerEvent) {
94291 me.on(triggerEvent, me.onItemTrigger, me);
94292 }
94293 if (triggerCtEvent) {
94294 me.on(triggerCtEvent, me.onContainerTrigger, me);
94295 }
94296
94297 container.element.on({
94298 delegate: '.' + me.getBaseCls() + '-disclosure',
94299 tap: 'handleItemDisclosure',
94300 scope: me
94301 });
94302
94303 container.element.on({
94304 resize: 'onContainerResize',
94305 scope: me
94306 });
94307
94308 // Android 2.x not a direct child
94309 container.innerElement.on({
94310 touchstart: 'onItemTouchStart',
94311 touchend: 'onItemTouchEnd',
94312 tap: 'onItemTap',
94313 taphold: 'onItemTapHold',
94314 singletap: 'onItemSingleTap',
94315 doubletap: 'onItemDoubleTap',
94316 swipe: 'onItemSwipe',
94317 delegate: '.' + Ext.baseCSSPrefix + 'list-item',
94318 scope: me
94319 });
94320
94321 if (me.getStore()) {
94322 me.refresh();
94323 }
94324 },
94325
94326 onTranslate: function(x, y) {
94327 var me = this,
94328 pinnedHeader = me.pinnedHeader,
94329 store = me.getStore(),
94330 storeCount = store && store.getCount(),
94331 grouped = me.getGrouped(),
94332 infinite = me.getInfinite();
94333
94334 if (!storeCount) {
94335 me.showEmptyText();
94336 me.showEmptyScrollDock();
94337
94338 pinnedHeader.$position = -10000;
94339 pinnedHeader.translate(0, -10000);
94340 }
94341 else if (infinite && me.itemsCount) {
94342 me.handleItemUpdates(y);
94343 me.handleItemHeights();
94344 me.handleItemTransforms();
94345
94346 if (!me.onIdleBound) {
94347 Ext.AnimationQueue.onIdle(me.onAnimationIdle, me);
94348 me.onIdleBound = true;
94349 }
94350 }
94351
94352 if (grouped && me.groups && me.groups.length && me.getPinHeaders()) {
94353 me.handlePinnedHeader(y);
94354 }
94355
94356 // This is a template method that can be intercepted by plugins to do things when scrolling
94357 this.onScrollBinder(x, y);
94358 },
94359
94360 onScrollBinder: function(){},
94361
94362 handleItemUpdates: function(y) {
94363 var me = this,
94364 listItems = me.listItems,
94365 itemsCount = listItems.length,
94366 info = me.getListItemInfo(),
94367 itemMap = me.getItemMap(),
94368 bufferSize = me.getBufferSize(),
94369 lastIndex = me.getStore().getCount() - 1,
94370 minimumBufferDistance = me.getMinimumBufferDistance(),
94371 currentTopVisibleIndex = me.topVisibleIndex,
94372 topRenderedIndex = me.topRenderedIndex,
94373 updateCount, i, item, topVisibleIndex, bufferDistance, itemIndex;
94374
94375 // This is the index of the item that is currently visible at the top
94376 me.topVisibleIndex = topVisibleIndex = Math.max(0, itemMap.findIndex(-y) || 0);
94377
94378 if (currentTopVisibleIndex !== topVisibleIndex) {
94379 // When we are scrolling up
94380 if (currentTopVisibleIndex > topVisibleIndex) {
94381 bufferDistance = topVisibleIndex - topRenderedIndex;
94382 if (bufferDistance < minimumBufferDistance) {
94383 updateCount = Math.min(itemsCount, minimumBufferDistance - bufferDistance);
94384
94385 if (updateCount == itemsCount) {
94386 me.topRenderedIndex = topRenderedIndex = Math.max(0, topVisibleIndex - (bufferSize - minimumBufferDistance));
94387 // Update all
94388 for (i = 0; i < updateCount; i++) {
94389 itemIndex = topRenderedIndex + i;
94390 item = listItems[i];
94391 me.updateListItem(item, itemIndex, info);
94392 }
94393 }
94394 else {
94395 for (i = 0; i < updateCount; i++) {
94396 itemIndex = topRenderedIndex - i - 1;
94397 if (itemIndex < 0) {
94398 break;
94399 }
94400
94401 item = listItems.pop();
94402 listItems.unshift(item);
94403 me.updateListItem(item, itemIndex, info);
94404 me.topRenderedIndex--;
94405 }
94406 }
94407 }
94408 }
94409 // When we are scrolling down
94410 else {
94411 bufferDistance = bufferSize - (topVisibleIndex - topRenderedIndex);
94412
94413 if (bufferDistance < minimumBufferDistance) {
94414 updateCount = Math.min(itemsCount, minimumBufferDistance - bufferDistance);
94415
94416 if (updateCount == itemsCount) {
94417 me.topRenderedIndex = topRenderedIndex = Math.min(lastIndex - itemsCount, topVisibleIndex - minimumBufferDistance);
94418 // Update all
94419 for (i = 0; i < updateCount; i++) {
94420 itemIndex = topRenderedIndex + i;
94421 item = listItems[i];
94422 me.updateListItem(item, itemIndex, info);
94423 }
94424 }
94425 else {
94426 for (i = 0; i < updateCount; i++) {
94427 itemIndex = topRenderedIndex + itemsCount + i;
94428 if (itemIndex > lastIndex) {
94429 break;
94430 }
94431
94432 item = listItems.shift();
94433 listItems.push(item);
94434 me.updateListItem(item, itemIndex, info);
94435 me.topRenderedIndex++;
94436 }
94437 }
94438 }
94439 }
94440 }
94441 },
94442
94443 onAnimationIdle: function() {
94444 var me = this,
94445 info = me.getListItemInfo(),
94446 bufferSize = me.getBufferSize(),
94447 topVisibleIndex = me.topVisibleIndex,
94448 topRenderedIndex = me.topRenderedIndex,
94449 lastIndex = me.getStore().getCount() - 1,
94450 listItems = me.listItems,
94451 itemsCount = listItems.length,
94452 topBufferDistance, bottomBufferDistance,
94453 i, ln, item, itemIndex;
94454
94455 topBufferDistance = topVisibleIndex - topRenderedIndex;
94456 bottomBufferDistance = topRenderedIndex + bufferSize - topVisibleIndex;
94457
94458 if (topBufferDistance < bottomBufferDistance) {
94459 // This means there are more items below the visible list. The user
94460 // has probably just scrolled up. In this case we move some items
94461 // from the bottom to the top only if the list is scrolled down a bit
94462 if (topVisibleIndex > 0) {
94463 ln = bottomBufferDistance - topBufferDistance;
94464
94465 for (i = 0; i < ln; i++) {
94466 itemIndex = topRenderedIndex - i - 1;
94467 if (itemIndex < 0) {
94468 break;
94469 }
94470
94471 item = listItems.pop();
94472 listItems.unshift(item);
94473 me.updateListItem(item, itemIndex, info);
94474 me.topRenderedIndex--;
94475 }
94476 }
94477 }
94478 else {
94479 ln = topBufferDistance - bottomBufferDistance;
94480 for (i = 0; i < ln; i++) {
94481 itemIndex = topRenderedIndex + itemsCount + i;
94482 if (itemIndex > lastIndex) {
94483 break;
94484 }
94485
94486 item = listItems.shift();
94487 listItems.push(item);
94488 me.updateListItem(item, itemIndex, info);
94489 me.topRenderedIndex++;
94490 }
94491 }
94492
94493 me.handleItemHeights();
94494 me.handleItemTransforms();
94495
94496 me.onIdleBound = false;
94497 },
94498
94499 handleItemHeights: function() {
94500 var me = this,
94501 updatedItems = me.updatedItems,
94502 ln = updatedItems.length,
94503 itemMap = me.getItemMap(),
94504 useSimpleItems = me.getUseSimpleItems(),
94505 minimumHeight = itemMap.getMinimumHeight(),
94506 headerIndices = me.headerIndices,
94507 headerMap = me.headerMap,
94508 variableHeights = me.getVariableHeights(),
94509 itemIndex, i, j, jln, item, height, scrollDockHeight;
94510
94511 for (i = 0; i < ln; i++) {
94512 item = updatedItems[i];
94513 itemIndex = item.$dataIndex;
94514
94515 // itemIndex may not be set yet if the store is still being loaded
94516 if (itemIndex !== null) {
94517 if (variableHeights) {
94518 height = useSimpleItems ? item.element.getHeight() : item.element.getFirstChild().getHeight();
94519 height = Math.max(height, minimumHeight);
94520 } else {
94521 height = minimumHeight;
94522 }
94523
94524 item.$ownItemHeight = height;
94525
94526 jln = me.scrollDockItems.top.length;
94527 if (item.isFirst) {
94528 me.totalScrollDockTopHeight = 0;
94529 for (j = 0; j < jln; j++) {
94530 scrollDockHeight = me.scrollDockItems.top[j].$scrollDockHeight;
94531 height += scrollDockHeight;
94532 me.totalScrollDockTopHeight += scrollDockHeight;
94533 }
94534 }
94535
94536 jln = me.scrollDockItems.bottom.length;
94537 if (item.isLast) {
94538 for (j = 0; j < jln; j++) {
94539 scrollDockHeight = me.scrollDockItems.bottom[j].$scrollDockHeight;
94540 height += scrollDockHeight;
94541 }
94542 }
94543
94544 if (headerIndices && headerIndices[itemIndex]) {
94545 height += me.headerHeight;
94546 }
94547
94548 itemMap.setItemHeight(itemIndex, height);
94549 item.$height = height;
94550 }
94551 }
94552
94553 itemMap.update();
94554 height = itemMap.getTotalHeight();
94555
94556 headerMap.length = 0;
94557 for (i in headerIndices) {
94558 if (headerIndices.hasOwnProperty(i)) {
94559 headerMap.push(itemMap.map[i]);
94560 }
94561 }
94562
94563 me.setScrollerHeight(height);
94564
94565 me.updatedItems.length = 0;
94566 },
94567
94568 setScrollerHeight: function(height) {
94569 var me = this,
94570 scroller = me.container.getScrollable().getScroller(),
94571 translatable = scroller.getTranslatable();
94572
94573 if (height != scroller.givenSize) {
94574 scroller.setSize(height);
94575 scroller.refreshMaxPosition();
94576 scroller.fireEvent('refresh', scroller);
94577
94578 if (translatable.isAnimating && translatable.activeEasingY && translatable.activeEasingY.setMinMomentumValue) {
94579 translatable.activeEasingY.setMinMomentumValue(-scroller.getMaxPosition().y);
94580 }
94581 }
94582 },
94583
94584 handleItemTransforms: function() {
94585 var me = this,
94586 listItems = me.listItems,
94587 itemsCount = listItems.length,
94588 itemMap = me.getItemMap(),
94589 scrollDockItems = me.scrollDockItems,
94590 grouped = me.getGrouped(),
94591 item, transY, i, jln, j;
94592
94593 for (i = 0; i < itemsCount; i++) {
94594 item = listItems[i];
94595 transY = itemMap.map[item.$dataIndex];
94596
94597 if (!item.$hidden && item.$position !== transY) {
94598 item.$position = transY;
94599
94600 jln = scrollDockItems.top.length;
94601 if (item.isFirst && jln) {
94602 for (j = 0; j < jln; j++) {
94603 scrollDockItems.top[j].translate(0, transY);
94604 transY += scrollDockItems.top[j].$scrollDockHeight;
94605 }
94606 }
94607
94608 if (grouped && me.headerIndices && me.headerIndices[item.$dataIndex]) {
94609 item.getHeader().translate(0, transY);
94610 transY += me.headerHeight;
94611 }
94612
94613 item.translate(0, transY);
94614 transY += item.$ownItemHeight;
94615
94616 jln = scrollDockItems.bottom.length;
94617 if (item.isLast && jln) {
94618 for (j = 0; j < jln; j++) {
94619 scrollDockItems.bottom[j].translate(0, transY);
94620 transY += scrollDockItems.bottom[j].$scrollDockHeight;
94621 }
94622 }
94623 }
94624 }
94625 },
94626
94627 handlePinnedHeader: function(y) {
94628 var me = this,
94629 pinnedHeader = me.pinnedHeader,
94630 itemMap = me.getItemMap(),
94631 groups = me.groups,
94632 headerMap = me.headerMap,
94633 headerHeight = me.headerHeight,
94634 store = me.getStore(),
94635 totalScrollDockTopHeight = me.totalScrollDockTopHeight,
94636 record, closestHeader, pushedHeader, transY, headerString;
94637
94638 closestHeader = itemMap.binarySearch(headerMap, -y);
94639 record = groups[closestHeader].children[0];
94640
94641 if (record) {
94642 pushedHeader = y + headerMap[closestHeader + 1] - headerHeight;
94643 // Top of the list or above (hide the floating header offscreen)
94644 if (y >= 0 || (closestHeader === 0 && totalScrollDockTopHeight + y >= 0) || (closestHeader === 0 && -y <= headerMap[closestHeader])) {
94645 transY = -10000;
94646 }
94647 // Scroll the floating header a bit
94648 else if (pushedHeader < 0) {
94649 transY = pushedHeader;
94650 }
94651 // Stick to the top of the screen
94652 else {
94653 transY = Math.max(0, y);
94654 }
94655
94656 headerString = store.getGroupString(record);
94657
94658 if (pinnedHeader.$currentHeader != headerString) {
94659 pinnedHeader.setHtml(headerString);
94660 pinnedHeader.$currentHeader = headerString;
94661 }
94662
94663 if (pinnedHeader.$position != transY) {
94664 pinnedHeader.translate(0, transY);
94665 pinnedHeader.$position = transY;
94666 }
94667 }
94668 },
94669
94670 createItem: function(config) {
94671 var me = this,
94672 container = me.container,
94673 listItems = me.listItems,
94674 infinite = me.getInfinite(),
94675 scrollElement = me.scrollElement,
94676 item, header, itemCls;
94677
94678 item = Ext.factory(config);
94679 item.dataview = me;
94680 item.$height = config.minHeight;
94681
94682 if (!infinite) {
94683 itemCls = me.getBaseCls() + '-item-relative';
94684 item.addCls(itemCls);
94685 }
94686
94687 header = item.getHeader();
94688 if (!infinite) {
94689 header.addCls(itemCls);
94690 } else {
94691 header.setTranslatable({
94692 translationMethod: this.translationMethod
94693 });
94694 header.translate(0, -10000);
94695
94696 scrollElement.insertFirst(header.renderElement);
94697 }
94698
94699 container.doAdd(item);
94700 listItems.push(item);
94701
94702 return item;
94703 },
94704
94705 setItemsCount: function(itemsCount) {
94706 var me = this,
94707 listItems = me.listItems,
94708 config = me.getListItemConfig(),
94709 difference = itemsCount - listItems.length,
94710 i;
94711
94712 // This loop will create new items if the new itemsCount is higher than the amount of items we currently have
94713 for (i = 0; i < difference; i++) {
94714 me.createItem(config);
94715 }
94716
94717 // This loop will destroy unneeded items if the new itemsCount is lower than the amount of items we currently have
94718 for (i = difference; i < 0; i++) {
94719 listItems.pop().destroy();
94720 }
94721
94722 me.itemsCount = itemsCount;
94723
94724 // Finally we update all the list items with the correct content
94725 me.updateAllListItems();
94726
94727 //Android Stock bug where redraw is needed to show empty list
94728 if (Ext.browser.is.AndroidStock && me.container.element && itemsCount === 0 && difference !== 0) {
94729 me.container.element.redraw();
94730 }
94731
94732 return me.listItems;
94733 },
94734
94735 updateUi: function(newUi, oldUi) {
94736 if (newUi && newUi != oldUi && newUi == 'round') {
94737 this.setPinHeaders(false);
94738 }
94739
94740 this.callParent(arguments);
94741 },
94742
94743 updateListItem: function(item, index, info) {
94744 var me = this,
94745 record = info.store.getAt(index),
94746 headerIndices = me.headerIndices,
94747 footerIndices = me.footerIndices,
94748 header = item.getHeader(),
94749 scrollDockItems = me.scrollDockItems,
94750 updatedItems = me.updatedItems,
94751 currentItemCls = item.renderElement.classList.slice(),
94752 currentHeaderCls = header.renderElement.classList.slice(),
94753 infinite = me.getInfinite(),
94754 storeCount = info.store.getCount(),
94755 itemCls = [],
94756 headerCls = [],
94757 itemRemoveCls = [info.headerCls, info.footerCls, info.firstCls, info.lastCls, info.selectedCls, info.stripeCls],
94758 headerRemoveCls = [info.headerCls, info.footerCls, info.firstCls, info.lastCls],
94759 ln, i, scrollDockItem, classCache;
94760
94761 // When we update a list item, the header and scrolldocks can make it have to be retransformed.
94762 // For that reason we want to always set the position to -10000 so that the next time we translate
94763 // all the pieces are transformed to the correct location
94764 if (infinite) {
94765 item.$position = -10000;
94766 }
94767
94768 // We begin by hiding/showing the item and its header depending on a record existing at this index
94769 if (!record) {
94770 item.setRecord(null);
94771 if (infinite) {
94772 item.translate(0, -10000);
94773 } else {
94774 item.hide();
94775 }
94776
94777 if (infinite) {
94778 header.translate(0, -10000);
94779 } else {
94780 header.hide();
94781 }
94782 item.$hidden = true;
94783 return;
94784 } else if (item.$hidden) {
94785 if (!infinite) {
94786 item.show();
94787 }
94788 item.$hidden = false;
94789 }
94790
94791 if (infinite) {
94792 updatedItems.push(item);
94793 }
94794
94795 // If this item was previously used for the first record in the store, and now it will not be, then we hide
94796 // any scrollDockTop items and change the isFirst flag
94797 if (item.isFirst && index !== 0 && scrollDockItems.top.length) {
94798 for (i = 0, ln = scrollDockItems.top.length; i < ln; i++) {
94799 scrollDockItem = scrollDockItems.top[i];
94800 if (infinite) {
94801 scrollDockItem.translate(0, -10000);
94802 }
94803 }
94804 item.isFirst = false;
94805 }
94806
94807 // If this item was previously used for the last record in the store, and now it will not be, then we hide
94808 // any scrollDockBottom items and change the istLast flag
94809 if (item.isLast && index !== storeCount - 1 && scrollDockItems.bottom.length) {
94810 for (i = 0, ln = scrollDockItems.bottom.length; i < ln; i++) {
94811 scrollDockItem = scrollDockItems.bottom[i];
94812 if (infinite) {
94813 scrollDockItem.translate(0, -10000);
94814 }
94815 }
94816 item.isLast = false;
94817 }
94818
94819 // If the item is already bound to this record then we shouldn't have to do anything
94820 if (item.$dataIndex !== index) {
94821 item.$dataIndex = index;
94822 me.fireEvent('itemindexchange', me, record, index, item);
94823 }
94824
94825 // This is where we actually update the item with the record
94826 if (item.getRecord() === record) {
94827 item.updateRecord(record);
94828 } else {
94829 item.setRecord(record);
94830 }
94831
94832 if (me.isSelected(record)) {
94833 itemCls.push(info.selectedCls);
94834 }
94835
94836 if (info.grouped) {
94837 if (headerIndices[index]) {
94838 itemCls.push(info.headerCls);
94839 headerCls.push(info.headerCls);
94840 header.setHtml(info.store.getGroupString(record));
94841
94842 if (!infinite) {
94843 header.renderElement.insertBefore(item.renderElement);
94844 }
94845 header.show();
94846 } else {
94847 if (infinite) {
94848 header.translate(0, -10000);
94849 } else {
94850 header.hide();
94851 }
94852 }
94853 if (footerIndices[index]) {
94854 itemCls.push(info.footerCls);
94855 headerCls.push(info.footerCls);
94856 }
94857 }
94858
94859 if (!info.grouped) {
94860 header.hide();
94861 }
94862
94863 if (index === 0) {
94864 item.isFirst = true;
94865 itemCls.push(info.firstCls);
94866 headerCls.push(info.firstCls);
94867
94868 if (!info.grouped) {
94869 itemCls.push(info.headerCls);
94870 headerCls.push(info.headerCls);
94871 }
94872
94873 if (!infinite) {
94874 for (i = 0, ln = scrollDockItems.top.length; i < ln; i++) {
94875 scrollDockItem = scrollDockItems.top[i];
94876 if (info.grouped) {
94877 scrollDockItem.renderElement.insertBefore(header.renderElement);
94878 } else {
94879 scrollDockItem.renderElement.insertBefore(item.renderElement);
94880 }
94881 }
94882 }
94883 }
94884
94885 if (index === storeCount - 1) {
94886 item.isLast = true;
94887 itemCls.push(info.lastCls);
94888 headerCls.push(info.lastCls);
94889
94890 if (!info.grouped) {
94891 itemCls.push(info.footerCls);
94892 headerCls.push(info.footerCls);
94893 }
94894
94895 if (!infinite) {
94896 for (i = 0, ln = scrollDockItems.bottom.length; i < ln; i++) {
94897 scrollDockItem = scrollDockItems.bottom[i];
94898 scrollDockItem.renderElement.insertAfter(item.renderElement);
94899 }
94900 }
94901 }
94902
94903 if (info.striped && index % 2 == 1) {
94904 itemCls.push(info.stripeCls);
94905 }
94906
94907 if (currentItemCls) {
94908 for (i = 0; i < itemRemoveCls.length; i++) {
94909 Ext.Array.remove(currentItemCls, itemRemoveCls[i]);
94910 }
94911 itemCls = Ext.Array.merge(itemCls, currentItemCls);
94912 }
94913
94914 if (currentHeaderCls) {
94915 for (i = 0; i < headerRemoveCls.length; i++) {
94916 Ext.Array.remove(currentHeaderCls, headerRemoveCls[i]);
94917 }
94918 headerCls = Ext.Array.merge(headerCls, currentHeaderCls);
94919 }
94920
94921 classCache = itemCls.join(' ');
94922
94923 if (item.classCache !== classCache) {
94924 item.renderElement.setCls(itemCls);
94925 item.classCache = classCache;
94926 }
94927
94928 header.renderElement.setCls(headerCls);
94929 },
94930
94931 updateAllListItems: function() {
94932 var me = this,
94933 store = me.getStore(),
94934 items = me.listItems,
94935 info = me.getListItemInfo(),
94936 topRenderedIndex = me.topRenderedIndex,
94937 i, ln;
94938
94939 if (store) {
94940 for (i = 0, ln = items.length; i < ln; i++) {
94941 me.updateListItem(items[i], topRenderedIndex + i, info);
94942 }
94943 }
94944
94945 if (me.isPainted()) {
94946 if (me.getInfinite() && store && store.getCount()) {
94947 me.handleItemHeights();
94948 }
94949 me.refreshScroller();
94950 }
94951 },
94952
94953 doRefresh: function() {
94954 var me = this,
94955 infinite = me.getInfinite(),
94956 scroller = me.container.getScrollable().getScroller(),
94957 storeCount = me.getStore().getCount();
94958
94959 if (infinite) {
94960 me.getItemMap().populate(storeCount, this.topRenderedIndex);
94961 }
94962
94963 if (me.getGrouped()) {
94964 me.refreshHeaderIndices();
94965 }
94966
94967 // This will refresh the items on the screen with the new data
94968 if (storeCount) {
94969 me.hideScrollDockItems();
94970 me.hideEmptyText();
94971 if (!infinite) {
94972 me.setItemsCount(storeCount);
94973 if (me.getScrollToTopOnRefresh()) {
94974 scroller.scrollTo(0, 0);
94975 }
94976 } else {
94977 if (me.getScrollToTopOnRefresh()) {
94978 me.topRenderedIndex = 0;
94979 me.topVisibleIndex = 0;
94980 scroller.position.y = 0;
94981 }
94982 me.updateAllListItems();
94983 }
94984 } else {
94985 me.onStoreClear();
94986 }
94987 },
94988
94989 onContainerResize: function(container, size) {
94990 var me = this,
94991 currentVisibleCount = me.visibleCount;
94992
94993 if (!me.headerHeight) {
94994 me.headerHeight = parseInt(me.pinnedHeader.renderElement.getHeight(), 10);
94995 }
94996
94997 if (me.getInfinite()) {
94998 me.visibleCount = Math.ceil(size.height / me.getItemMap().getMinimumHeight());
94999 if (me.visibleCount != currentVisibleCount) {
95000 me.setItemsCount(me.visibleCount + me.getBufferSize());
95001 // This is a private event used by some plugins
95002 me.fireEvent('updatevisiblecount', this, me.visibleCount, currentVisibleCount);
95003 }
95004 } else if (me.listItems.length && me.getGrouped() && me.getPinHeaders()) {
95005 // Whenever the container resizes, headers might be in different locations. For this reason
95006 // we refresh the header position map
95007 me.updateHeaderMap();
95008 }
95009 },
95010
95011 refreshScroller: function() {
95012 var me = this;
95013
95014 if (me.isPainted()) {
95015 if (!me.getInfinite() && me.getGrouped() && me.getPinHeaders()) {
95016 me.updateHeaderMap();
95017 }
95018
95019 me.container.getScrollable().getScroller().refresh();
95020 }
95021 },
95022
95023 updateHeaderMap: function() {
95024 var me = this,
95025 headerMap = me.headerMap,
95026 headerIndices = me.headerIndices,
95027 header, i;
95028
95029 headerMap.length = 0;
95030 for (i in headerIndices) {
95031 if (headerIndices.hasOwnProperty(i)) {
95032 header = me.getItemAt(i).getHeader();
95033 headerMap.push(header.renderElement.dom.offsetTop);
95034 }
95035 }
95036 },
95037
95038 applyVariableHeights: function(value) {
95039 if (!this.getInfinite()) {
95040 return true;
95041 }
95042 return value;
95043 },
95044
95045 applyDefaultType: function(defaultType) {
95046 if (!defaultType) {
95047 defaultType = this.getUseSimpleItems() ? 'simplelistitem' : 'listitem';
95048 }
95049 return defaultType;
95050 },
95051
95052 applyItemMap: function(itemMap) {
95053 return Ext.factory(itemMap, Ext.util.PositionMap, this.getItemMap());
95054 },
95055
95056 updateItemHeight: function(itemHeight) {
95057 this.getItemMap().setMinimumHeight(itemHeight);
95058 },
95059
95060 applyIndexBar: function(indexBar) {
95061 return Ext.factory(indexBar, Ext.dataview.IndexBar, this.getIndexBar());
95062 },
95063
95064 updatePinHeaders: function(pinnedHeaders) {
95065 if (this.isPainted()) {
95066 this.pinnedHeader.translate(0, pinnedHeaders ? this.pinnedHeader.$position : -10000);
95067 }
95068 },
95069
95070 updateItemTpl: function(newTpl) {
95071 var me = this,
95072 listItems = me.listItems,
95073 ln = listItems.length || 0,
95074 i, listItem;
95075
95076 for (i = 0; i < ln; i++) {
95077 listItem = listItems[i];
95078 listItem.setTpl(newTpl);
95079 }
95080
95081 me.updateAllListItems();
95082 },
95083
95084 updateItemCls: function(newCls, oldCls) {
95085 var items = this.listItems,
95086 ln = items.length,
95087 i, item;
95088
95089 for (i = 0; i < ln; i++) {
95090 item = items[i];
95091 item.removeCls(oldCls);
95092 item.addCls(newCls);
95093 }
95094 },
95095
95096 updateIndexBar: function(indexBar, oldIndexBar) {
95097 var me = this,
95098 scrollViewElement = me.scrollViewElement;
95099
95100 if (oldIndexBar) {
95101 oldIndexBar.un({
95102 index: 'onIndex',
95103 scope: me
95104 });
95105
95106 if (!indexBar) {
95107 me.element.removeCls(me.getBaseCls() + '-indexed');
95108 }
95109
95110 if (scrollViewElement) {
95111 scrollViewElement.removeChild(oldIndexBar.renderElement);
95112 }
95113 }
95114
95115 if (indexBar) {
95116 indexBar.on({
95117 index: 'onIndex',
95118 scope: me
95119 });
95120
95121 if (!oldIndexBar) {
95122 me.element.addCls(me.getBaseCls() + '-indexed');
95123 }
95124
95125 if (scrollViewElement) {
95126 scrollViewElement.appendChild(indexBar.renderElement);
95127 }
95128 }
95129 },
95130
95131 updateGrouped: function(grouped) {
95132 var me = this,
95133 baseCls = this.getBaseCls(),
95134 pinnedHeader = me.pinnedHeader,
95135 cls = baseCls + '-grouped',
95136 unCls = baseCls + '-ungrouped';
95137
95138 if (pinnedHeader) {
95139 pinnedHeader.translate(0, -10000);
95140 }
95141
95142 if (grouped) {
95143 me.addCls(cls);
95144 me.removeCls(unCls);
95145 }
95146 else {
95147 me.addCls(unCls);
95148 me.removeCls(cls);
95149 }
95150
95151 if (me.getInfinite()) {
95152 me.refreshHeaderIndices();
95153 me.handleItemHeights();
95154 }
95155 me.updateAllListItems();
95156 },
95157
95158 onStoreAdd: function() {
95159 this.doRefresh();
95160 },
95161
95162 onStoreRemove: function() {
95163 this.doRefresh();
95164 },
95165
95166 onStoreUpdate: function(store, record, newIndex, oldIndex) {
95167 var me = this,
95168 item;
95169
95170 oldIndex = (typeof oldIndex === 'undefined') ? newIndex : oldIndex;
95171
95172 if (me.getInfinite() || (oldIndex !== newIndex)) {
95173 me.doRefresh();
95174 }
95175 else {
95176 item = me.listItems[newIndex];
95177 if (item) {
95178 me.updateListItem(item, newIndex, me.getListItemInfo());
95179 }
95180 }
95181 },
95182
95183 onStoreClear: function() {
95184 var me = this,
95185 scroller = me.container.getScrollable().getScroller(),
95186 infinite = me.getInfinite();
95187
95188 if (me.pinnedHeader) {
95189 me.pinnedHeader.translate(0, -10000);
95190 }
95191
95192 if (!infinite) {
95193 me.setItemsCount(0);
95194 scroller.scrollTo(0, 0);
95195 } else {
95196 me.topRenderedIndex = 0;
95197 me.topVisibleIndex = 0;
95198 scroller.position.y = 0;
95199 me.updateAllListItems();
95200 }
95201 },
95202
95203 showEmptyScrollDock: function() {
95204 var me = this,
95205 infinite = me.getInfinite(),
95206 scrollDockItems = me.scrollDockItems,
95207 offset = 0,
95208 i, ln, item;
95209
95210 for (i = 0, ln = scrollDockItems.top.length; i < ln; i++) {
95211 item = scrollDockItems.top[i];
95212 if (infinite) {
95213 item.translate(0, offset);
95214 offset += item.$scrollDockHeight;
95215 } else {
95216 this.scrollElement.appendChild(item.renderElement);
95217 }
95218 }
95219
95220 for (i = 0, ln = scrollDockItems.bottom.length; i < ln; i++) {
95221 item = scrollDockItems.bottom[i];
95222 if (infinite) {
95223 item.translate(0, offset);
95224 offset += item.$scrollDockHeight;
95225 } else {
95226 this.scrollElement.appendChild(item.renderElement);
95227 }
95228 }
95229 },
95230
95231 hideScrollDockItems: function() {
95232 var me = this,
95233 infinite = me.getInfinite(),
95234 scrollDockItems = me.scrollDockItems,
95235 i, ln, item;
95236
95237 if (!infinite) {
95238 return;
95239 }
95240
95241 for (i = 0, ln = scrollDockItems.top.length; i < ln; i++) {
95242 item = scrollDockItems.top[i];
95243 item.translate(0, -10000);
95244 }
95245
95246 for (i = 0, ln = scrollDockItems.bottom.length; i < ln; i++) {
95247 item = scrollDockItems.bottom[i];
95248 item.translate(0, -10000);
95249 }
95250 },
95251
95252 /**
95253 * Returns an item at the specified index.
95254 * @param {Number} index Index of the item.
95255 * @return {Ext.dom.Element/Ext.dataview.component.DataItem} item Item at the specified index.
95256 */
95257 getItemAt: function(index) {
95258 var listItems = this.listItems,
95259 ln = listItems.length,
95260 i, listItem;
95261
95262 for (i = 0; i < ln; i++) {
95263 listItem = listItems[i];
95264 if (listItem.$dataIndex == index) {
95265 return listItem;
95266 }
95267 }
95268 },
95269
95270 /**
95271 * Returns an index for the specified item.
95272 * @param {Number} item The item to locate.
95273 * @return {Number} Index for the specified item.
95274 */
95275 getItemIndex: function(item) {
95276 return item.$dataIndex;
95277 },
95278
95279 /**
95280 * Returns an array of the current items in the DataView.
95281 * @return {Ext.dom.Element[]/Ext.dataview.component.DataItem[]} Array of Items.
95282 */
95283 getViewItems: function() {
95284 return this.listItems;
95285 },
95286
95287 getListItemInfo: function() {
95288 var me = this,
95289 baseCls = me.getBaseCls();
95290
95291 return {
95292 store: me.getStore(),
95293 grouped: me.getGrouped(),
95294 baseCls: baseCls,
95295 selectedCls: me.getSelectedCls(),
95296 headerCls: baseCls + '-header-wrap',
95297 footerCls: baseCls + '-footer-wrap',
95298 firstCls: baseCls + '-item-first',
95299 lastCls: baseCls + '-item-last',
95300 stripeCls: baseCls + '-item-odd',
95301 striped: me.getStriped(),
95302 itemMap: me.getItemMap(),
95303 defaultItemHeight: me.getItemHeight()
95304 };
95305 },
95306
95307 getListItemConfig: function() {
95308 var me = this,
95309 minimumHeight = me.getItemMap().getMinimumHeight(),
95310 config = {
95311 xtype: me.getDefaultType(),
95312 itemConfig: me.getItemConfig(),
95313 tpl: me.getItemTpl(),
95314 minHeight: minimumHeight,
95315 cls: me.getItemCls()
95316 };
95317
95318 if (me.getInfinite()) {
95319 config.translatable = {
95320 translationMethod: this.translationMethod
95321 };
95322 }
95323
95324 if (!me.getVariableHeights()) {
95325 config.height = minimumHeight;
95326 }
95327
95328 return config;
95329 },
95330
95331 refreshHeaderIndices: function() {
95332 var me = this,
95333 store = me.getStore(),
95334 storeLn = store && store.getCount(),
95335 groups = store.getGrouper() ? store.getGroups() : null,
95336 grouped = me.getGrouped(),
95337 headerIndices = me.headerIndices = {},
95338 footerIndices = me.footerIndices = {},
95339 i, previousIndex, firstGroupedRecord, storeIndex, groupLn;
95340
95341 if (!grouped || !groups) {
95342 return footerIndices;
95343 }
95344 groupLn = groups.length;
95345 me.groups = groups;
95346
95347 for (i = 0; i < groupLn; i++) {
95348 firstGroupedRecord = groups[i].children[0];
95349 storeIndex = store.indexOf(firstGroupedRecord);
95350 headerIndices[storeIndex] = true;
95351
95352 previousIndex = storeIndex - 1;
95353 if (previousIndex >= 0) {
95354 footerIndices[previousIndex] = true;
95355 }
95356 }
95357
95358 footerIndices[storeLn - 1] = true;
95359
95360 return headerIndices;
95361 },
95362
95363 onIndex: function(indexBar, index) {
95364 var me = this,
95365 key = index.toLowerCase(),
95366 store = me.getStore(),
95367 groups = store.getGroups(),
95368 ln = groups.length,
95369 group, i, closest, id;
95370
95371 for (i = 0; i < ln; i++) {
95372 group = groups[i];
95373 id = group.name.toLowerCase();
95374 if (id >= key) {
95375 closest = group;
95376 break;
95377 }
95378 else {
95379 closest = group;
95380 }
95381 }
95382
95383 if (closest) {
95384 this.scrollToRecord(closest.children[0]);
95385 }
95386 },
95387
95388 /**
95389 *
95390 * Scrolls the list so that the specified record is at the top.
95391 *
95392 * @param record {Ext.data.Model} Record in the lists store to scroll to
95393 * @param animate {Boolean} Determines if scrolling is animated to a cut
95394 * @param overscroll {Boolean} Determines if list can be overscrolled
95395 */
95396 scrollToRecord: function(record, animate, overscroll) {
95397 var me = this,
95398 scroller = me.container.getScrollable().getScroller(),
95399 store = me.getStore(),
95400 index = store.indexOf(record);
95401
95402 //stop the scroller from scrolling
95403 scroller.stopAnimation();
95404
95405 //make sure the new offsetTop is not out of bounds for the scroller
95406 var containerSize = scroller.getContainerSize().y,
95407 size = scroller.getSize().y,
95408 maxOffset = size - containerSize,
95409 offset, item;
95410
95411 if (me.getInfinite()) {
95412 offset = me.getItemMap().map[index];
95413 }
95414 else {
95415 item = me.listItems[index];
95416 if (item.getHeader().isPainted()) {
95417 offset = item.getHeader().renderElement.dom.offsetTop;
95418 }
95419 else {
95420 offset = item.renderElement.dom.offsetTop;
95421 }
95422 }
95423
95424 if (!overscroll) {
95425 offset = Math.min(offset, maxOffset);
95426 }
95427
95428 scroller.scrollTo(0, offset, !!animate);
95429 },
95430
95431 onItemAdd: function(item) {
95432 var me = this,
95433 config = item.config;
95434
95435 if (config.scrollDock) {
95436 if (config.scrollDock == 'bottom') {
95437 me.scrollDockItems.bottom.push(item);
95438 } else {
95439 me.scrollDockItems.top.push(item);
95440 }
95441
95442 if (me.getInfinite()) {
95443 item.on({
95444 resize: 'onScrollDockItemResize',
95445 scope: this
95446 });
95447
95448 item.addCls(me.getBaseCls() + '-scrolldockitem');
95449 item.setTranslatable({
95450 translationMethod: this.translationMethod
95451 });
95452 item.translate(0, -10000);
95453 item.$scrollDockHeight = 0;
95454 }
95455
95456 me.container.doAdd(item);
95457 } else {
95458 me.callParent(arguments);
95459 }
95460 },
95461
95462 /**
95463 * Returns all the items that are docked in the scroller in this list.
95464 * @return {Array} An array of the scrollDock items
95465 */
95466 getScrollDockedItems: function() {
95467 return this.scrollDockItems.bottom.slice().concat(this.scrollDockItems.top.slice());
95468 },
95469
95470 onScrollDockItemResize: function(dockItem, size) {
95471 var me = this,
95472 items = me.listItems,
95473 ln = items.length,
95474 i, item;
95475
95476 Ext.getCmp(dockItem.id).$scrollDockHeight = size.height;
95477
95478 for (i = 0; i < ln; i++) {
95479 item = items[i];
95480 if (item.isLast) {
95481 this.updatedItems.push(item);
95482 this.refreshScroller();
95483 break;
95484 }
95485 }
95486 },
95487
95488 onItemTouchStart: function(e) {
95489 this.container.innerElement.on({
95490 touchmove: 'onItemTouchMove',
95491 delegate: '.' + Ext.baseCSSPrefix + 'list-item',
95492 single: true,
95493 scope: this
95494 });
95495 this.callParent(this.parseEvent(e));
95496 },
95497
95498 onItemTouchMove: function(e) {
95499 this.callParent(this.parseEvent(e));
95500 },
95501
95502 onItemTouchEnd: function(e) {
95503 this.container.innerElement.un({
95504 touchmove: 'onItemTouchMove',
95505 delegate: '.' + Ext.baseCSSPrefix + 'list-item',
95506 scope: this
95507 });
95508 this.callParent(this.parseEvent(e));
95509 },
95510
95511 onItemTap: function(e) {
95512 this.callParent(this.parseEvent(e));
95513 },
95514
95515 onItemTapHold: function(e) {
95516 this.callParent(this.parseEvent(e));
95517 },
95518
95519 onItemSingleTap: function(e) {
95520 this.callParent(this.parseEvent(e));
95521 },
95522
95523 onItemDoubleTap: function(e) {
95524 this.callParent(this.parseEvent(e));
95525 },
95526
95527 onItemSwipe: function(e) {
95528 this.callParent(this.parseEvent(e));
95529 },
95530
95531 parseEvent: function(e) {
95532 var me = this,
95533 target = Ext.fly(e.getTarget()).findParent('.' + Ext.baseCSSPrefix + 'list-item', 8),
95534 item = Ext.getCmp(target.id);
95535
95536 return [me, item, item.$dataIndex, e];
95537 },
95538
95539 doItemSelect: function(me, record) {
95540 this.callParent(arguments);
95541
95542 var item = me.getItemAt(me.getStore().indexOf(record));
95543 if (me.container && !me.isDestroyed && item && item.isComponent) {
95544 item.classCache = item.renderElement.classList.slice();
95545 }
95546 },
95547
95548 doItemDeselect: function(me, record) {
95549 this.callParent(arguments);
95550
95551 var item = me.getItemAt(me.getStore().indexOf(record));
95552 if (item && item.isComponent) {
95553 item.classCache = item.renderElement.classList.slice();
95554 }
95555 },
95556
95557 applyOnItemDisclosure: function(config) {
95558 if (Ext.isFunction(config)) {
95559 return {
95560 scope: this,
95561 handler: config
95562 };
95563 }
95564 return config;
95565 },
95566
95567 handleItemDisclosure: function(e) {
95568 var me = this,
95569 item = Ext.getCmp(Ext.get(e.getTarget()).up('.x-list-item').id),
95570 index = item.$dataIndex,
95571 record = me.getStore().getAt(index);
95572
95573 me.fireAction('disclose', [me, record, item, index, e], 'doDisclose');
95574 },
95575
95576 doDisclose: function(me, record, item, index, e) {
95577 var onItemDisclosure = me.getOnItemDisclosure();
95578
95579 if (onItemDisclosure && onItemDisclosure.handler) {
95580 onItemDisclosure.handler.call(onItemDisclosure.scope || me, record, item, index, e);
95581 }
95582 },
95583
95584 // apply to the selection model to maintain visual UI cues
95585 onItemTrigger: function(me, index, target, record, e) {
95586 if (!(this.getPreventSelectionOnDisclose() && Ext.fly(e.target).hasCls(this.getBaseCls() + '-disclosure'))) {
95587 this.callParent(arguments);
95588 }
95589 },
95590
95591 destroy: function() {
95592 var me = this,
95593 items = me.listItems,
95594 ln = items.length,
95595 i;
95596
95597 me.callParent(arguments);
95598
95599 if (me.onIdleBound) {
95600 Ext.AnimationQueue.unIdle(me.onAnimationIdle, me);
95601 }
95602
95603 for (i = 0; i < ln; i++) {
95604 items[i].destroy();
95605 }
95606 me.listItems = null;
95607 }
95608 });
95609
95610 /**
95611 * NestedList provides a miller column interface to navigate between nested sets
95612 * and provide a clean interface with limited screen real-estate.
95613 *
95614 * @example miniphone preview
95615 * var data = {
95616 * text: 'Groceries',
95617 * items: [{
95618 * text: 'Drinks',
95619 * items: [{
95620 * text: 'Water',
95621 * items: [{
95622 * text: 'Sparkling',
95623 * leaf: true
95624 * }, {
95625 * text: 'Still',
95626 * leaf: true
95627 * }]
95628 * }, {
95629 * text: 'Coffee',
95630 * leaf: true
95631 * }, {
95632 * text: 'Espresso',
95633 * leaf: true
95634 * }, {
95635 * text: 'Redbull',
95636 * leaf: true
95637 * }, {
95638 * text: 'Coke',
95639 * leaf: true
95640 * }, {
95641 * text: 'Diet Coke',
95642 * leaf: true
95643 * }]
95644 * }, {
95645 * text: 'Fruit',
95646 * items: [{
95647 * text: 'Bananas',
95648 * leaf: true
95649 * }, {
95650 * text: 'Lemon',
95651 * leaf: true
95652 * }]
95653 * }, {
95654 * text: 'Snacks',
95655 * items: [{
95656 * text: 'Nuts',
95657 * leaf: true
95658 * }, {
95659 * text: 'Pretzels',
95660 * leaf: true
95661 * }, {
95662 * text: 'Wasabi Peas',
95663 * leaf: true
95664 * }]
95665 * }]
95666 * };
95667 *
95668 * Ext.define('ListItem', {
95669 * extend: 'Ext.data.Model',
95670 * config: {
95671 * fields: [{
95672 * name: 'text',
95673 * type: 'string'
95674 * }]
95675 * }
95676 * });
95677 *
95678 * var store = Ext.create('Ext.data.TreeStore', {
95679 * model: 'ListItem',
95680 * defaultRootProperty: 'items',
95681 * root: data
95682 * });
95683 *
95684 * var nestedList = Ext.create('Ext.NestedList', {
95685 * fullscreen: true,
95686 * title: 'Groceries',
95687 * displayField: 'text',
95688 * store: store
95689 * });
95690 *
95691 *
95692 * ###Further Reading
95693 * [Sencha Touch Nested List Guide](../../../components/nested_list.html)
95694 */
95695 Ext.define('Ext.dataview.NestedList', {
95696 alternateClassName: 'Ext.NestedList',
95697 extend: Ext.Container ,
95698 xtype: 'nestedlist',
95699
95700
95701
95702
95703
95704
95705
95706
95707
95708
95709 config: {
95710 /**
95711 * @cfg
95712 * @inheritdoc
95713 */
95714 baseCls: Ext.baseCSSPrefix + 'nested-list',
95715
95716 /**
95717 * @cfg {String/Object/Boolean} cardSwitchAnimation
95718 * Animation to be used during transitions of cards.
95719 * @removed 2.0.0 please use {@link Ext.layout.Card#animation}
95720 */
95721
95722 /**
95723 * @cfg {String} backText
95724 * The label to display for the back button.
95725 * @accessor
95726 */
95727 backText: 'Back',
95728
95729 /**
95730 * @cfg {Boolean} useTitleAsBackText
95731 * `true` to use title as a label for back button.
95732 * @accessor
95733 */
95734 useTitleAsBackText: true,
95735
95736 /**
95737 * @cfg {Boolean} updateTitleText
95738 * Update the title with the currently selected category.
95739 * @accessor
95740 */
95741 updateTitleText: true,
95742
95743 /**
95744 * @cfg {String} displayField
95745 * Display field to use when setting item text and title.
95746 * This configuration is ignored when overriding {@link #getItemTextTpl} or
95747 * {@link #getTitleTextTpl} for the item text or title.
95748 * @accessor
95749 */
95750 displayField: 'text',
95751
95752 /**
95753 * @cfg {String} loadingText
95754 * Loading text to display when a subtree is loading.
95755 * @accessor
95756 */
95757 loadingText: 'Loading...',
95758
95759 /**
95760 * @cfg {String} emptyText
95761 * Empty text to display when a subtree is empty.
95762 * @accessor
95763 */
95764 emptyText: 'No items available.',
95765
95766 /**
95767 * @cfg {Boolean/Function} onItemDisclosure
95768 * Maps to the {@link Ext.List#onItemDisclosure} configuration for individual lists.
95769 * @accessor
95770 */
95771 onItemDisclosure: false,
95772
95773 /**
95774 * @cfg {Boolean} allowDeselect
95775 * Set to `true` to allow the user to deselect leaf items via interaction.
95776 * @accessor
95777 */
95778 allowDeselect: false,
95779
95780 /**
95781 * @deprecated 2.0.0 Please set the {@link #toolbar} configuration to `false` instead
95782 * @cfg {Boolean} useToolbar `true` to show the header toolbar.
95783 * @accessor
95784 */
95785 useToolbar: null,
95786
95787 /**
95788 * @cfg {Ext.Toolbar/Object/Boolean} toolbar
95789 * The configuration to be used for the toolbar displayed in this nested list.
95790 * @accessor
95791 */
95792 toolbar: {
95793 docked: 'top',
95794 xtype: 'titlebar',
95795 ui: 'light',
95796 inline: true
95797 },
95798
95799 /**
95800 * @cfg {String} title The title of the toolbar
95801 * @accessor
95802 */
95803 title: '',
95804
95805 /**
95806 * @cfg {String} layout
95807 * @hide
95808 * @accessor
95809 */
95810 layout: {
95811 type: 'card',
95812 animation: {
95813 type: 'slide',
95814 duration: 250,
95815 direction: 'left'
95816 }
95817 },
95818
95819 /**
95820 * @cfg {Ext.data.TreeStore/String} store The tree store to be used for this nested list.
95821 */
95822 store: null,
95823
95824 /**
95825 * @cfg {Ext.Container} detailContainer The container of the `detailCard`.
95826 * A detailContainer is a reference to the container where a detail card
95827 * displays.
95828 *
95829 * See [Sencha Touch Nested List Guide](../../../components/nested_list.html)
95830 * and http://en.wikipedia.org/wiki/Miller_columns
95831 *
95832 * The two possible values for a detailContainer are undefined (default),
95833 * which indicates that a detailCard appear in the same container, or you
95834 * can specify a new container location. The default condition uses the
95835 * current List container.
95836 *
95837 * The following example shows creating a location for a detailContainer:
95838 *
95839 * var detailContainer = Ext.create('Ext.Container', {
95840 * layout: 'card'
95841 * });
95842 *
95843 * var nestedList = Ext.create('Ext.NestedList', {
95844 * store: treeStore,
95845 * detailCard: true,
95846 * detailContainer: detailContainer
95847 * });
95848 *
95849 * The default value is typically used for phone devices in portrait mode
95850 * where the small screen size dictates that the detailCard replace the
95851 * current container.
95852 * @accessor
95853 */
95854 detailContainer: undefined,
95855
95856 /**
95857 * @cfg {Ext.Component} detailCard provides the information for a leaf
95858 * in a Miller column list. In a Miller column, users follow a
95859 * hierarchial tree structure to a leaf, which provides information
95860 * about the item in the list. The detailCard lists the information at
95861 * the leaf.
95862 *
95863 * See [Sencha Touch Nested List Guide](../../../components/nested_list.html)
95864 * and http://en.wikipedia.org/wiki/Miller_columns
95865 *
95866 * @accessor
95867 */
95868 detailCard: null,
95869
95870 /**
95871 * @cfg {Object} backButton The configuration for the back button used in the nested list.
95872 */
95873 backButton: {
95874 ui: 'back',
95875 hidden: true
95876 },
95877
95878 /**
95879 * @cfg {Object} listConfig An optional config object which is merged with the default
95880 * configuration used to create each nested list.
95881 */
95882 listConfig: null,
95883
95884 /**
95885 * @cfg {Boolean} useSimpleItems
95886 * Set this to false if you want the lists in this NestedList to create complex container list items.
95887 */
95888 useSimpleItems: true,
95889
95890 /**
95891 * @cfg {Number} itemHeight
95892 * This allows you to set the default item height and is used to roughly calculate the amount
95893 * of items needed to fill the list. By default items are around 50px high. If you set this
95894 * configuration in combination with setting the {@link #variableHeights} to false you
95895 * can improve the scrolling speed
95896 */
95897 itemHeight: 47,
95898
95899 /**
95900 * @cfg {Boolean} variableHeights
95901 * This configuration allows you optimize the picker by not having it read the DOM heights of list items.
95902 * Instead it will assume (and set) the height to be the {@link #itemHeight}.
95903 */
95904 variableHeights: false,
95905
95906 // @private
95907 lastNode: null,
95908
95909 // @private
95910 lastActiveList: null,
95911
95912 ui: null,
95913
95914 clearSelectionOnListChange: true
95915 },
95916
95917 platformConfig: [
95918 {
95919 theme: ['Windows'],
95920 itemHeight: 42
95921 },
95922 {
95923 theme: ['Cupertino'],
95924 itemHeight: 43,
95925 useTitleAsBackText: true,
95926 updateTitleText: false
95927 },
95928 {
95929 theme: ['Blackberry', 'Blackberry103'],
95930 toolbar: {
95931 splitNavigation: true
95932 }
95933 },
95934 {
95935 theme: ['Tizen'],
95936 backText: ''
95937 }
95938 ],
95939
95940 /**
95941 * @event itemtap
95942 * Fires when a node is tapped on.
95943 * @param {Ext.dataview.NestedList} this
95944 * @param {Ext.dataview.List} list The Ext.dataview.List that is currently active.
95945 * @param {Number} index The index of the item tapped.
95946 * @param {Ext.dom.Element} target The element tapped.
95947 * @param {Ext.data.Record} record The record tapped.
95948 * @param {Ext.event.Event} e The event object.
95949 */
95950
95951 /**
95952 * @event itemdoubletap
95953 * Fires when a node is double tapped on.
95954 * @param {Ext.dataview.NestedList} this
95955 * @param {Ext.dataview.List} list The Ext.dataview.List that is currently active.
95956 * @param {Number} index The index of the item that was tapped.
95957 * @param {Ext.dom.Element} target The element tapped.
95958 * @param {Ext.data.Record} record The record tapped.
95959 * @param {Ext.event.Event} e The event object.
95960 */
95961
95962 /**
95963 * @event containertap
95964 * Fires when a tap occurs and it is not on a template node.
95965 * @param {Ext.dataview.NestedList} this
95966 * @param {Ext.dataview.List} list The Ext.dataview.List that is currently active.
95967 * @param {Ext.event.Event} e The raw event object.
95968 */
95969
95970 /**
95971 * @event selectionchange
95972 * Fires when the selected nodes change.
95973 * @param {Ext.dataview.NestedList} this
95974 * @param {Ext.dataview.List} list The Ext.dataview.List that is currently active.
95975 * @param {Array} selections Array of the selected nodes.
95976 */
95977
95978 /**
95979 * @event beforeselectionchange
95980 * Fires before a selection is made.
95981 * @param {Ext.dataview.NestedList} this
95982 * @param {Ext.dataview.List} list The Ext.dataview.List that is currently active.
95983 * @param {HTMLElement} node The node to be selected.
95984 * @param {Array} selections Array of currently selected nodes.
95985 * @deprecated 2.0.0 Please listen to the {@link #selectionchange} event with an order of `before` instead.
95986 */
95987
95988 /**
95989 * @event listchange
95990 * Fires when the user taps a list item.
95991 * @param {Ext.dataview.NestedList} this
95992 * @param {Object} listitem The new active list.
95993 */
95994
95995 /**
95996 * @event leafitemtap
95997 * Fires when the user taps a leaf list item.
95998 * @param {Ext.dataview.NestedList} this
95999 * @param {Ext.List} list The subList the item is on.
96000 * @param {Number} index The index of the item tapped.
96001 * @param {Ext.dom.Element} target The element tapped.
96002 * @param {Ext.data.Record} record The record tapped.
96003 * @param {Ext.event.Event} e The event.
96004 */
96005
96006 /**
96007 * @event back
96008 * @preventable doBack
96009 * Fires when the user taps Back.
96010 * @param {Ext.dataview.NestedList} this
96011 * @param {HTMLElement} node The node to be selected.
96012 * @param {Ext.dataview.List} lastActiveList The Ext.dataview.List that was last active.
96013 * @param {Boolean} detailCardActive Flag set if the detail card is currently active.
96014 */
96015
96016 /**
96017 * @event beforeload
96018 * Fires before a request is made for a new data object.
96019 * @param {Ext.dataview.NestedList} this
96020 * @param {Ext.data.Store} store The store instance.
96021 * @param {Ext.data.Operation} operation The Ext.data.Operation object that will be passed to the Proxy to
96022 * load the Store.
96023 */
96024
96025 /**
96026 * @event load
96027 * Fires whenever records have been loaded into the store.
96028 * @param {Ext.dataview.NestedList} this
96029 * @param {Ext.data.Store} store The store instance.
96030 * @param {Ext.util.Grouper[]} records An array of records.
96031 * @param {Boolean} successful `true` if the operation was successful.
96032 * @param {Ext.data.Operation} operation The associated operation.
96033 */
96034 constructor: function (config) {
96035 if (Ext.isObject(config)) {
96036 if (config.getTitleTextTpl) {
96037 this.getTitleTextTpl = config.getTitleTextTpl;
96038 }
96039 if (config.getItemTextTpl) {
96040 this.getItemTextTpl = config.getItemTextTpl;
96041 }
96042 }
96043 this.callParent(arguments);
96044 },
96045
96046 onItemInteraction: function () {
96047 if (this.isGoingTo) {
96048 return false;
96049 }
96050 },
96051
96052 applyDetailContainer: function (config) {
96053 if (!config) {
96054 config = this;
96055 }
96056
96057 return config;
96058 },
96059
96060 updateDetailContainer: function (newContainer, oldContainer) {
96061 if (newContainer) {
96062 newContainer.onBefore('activeitemchange', 'onBeforeDetailContainerChange', this);
96063 newContainer.onAfter('activeitemchange', 'onDetailContainerChange', this);
96064 }
96065 },
96066
96067 onBeforeDetailContainerChange: function () {
96068 this.isGoingTo = true;
96069 },
96070
96071 onDetailContainerChange: function () {
96072 this.isGoingTo = false;
96073 },
96074
96075 /**
96076 * Called when an list item has been tapped.
96077 * @param {Ext.List} list The subList the item is on.
96078 * @param {Number} index The id of the item tapped.
96079 * @param {Ext.Element} target The list item tapped.
96080 * @param {Ext.data.Record} record The record which as tapped.
96081 * @param {Ext.event.Event} e The event.
96082 */
96083 onItemTap: function (list, index, target, record, e) {
96084 var me = this,
96085 store = list.getStore(),
96086 node = store.getAt(index);
96087
96088 me.fireEvent('itemtap', this, list, index, target, record, e);
96089 if (node.isLeaf()) {
96090 me.fireEvent('leafitemtap', this, list, index, target, record, e);
96091 me.goToLeaf(node);
96092 }
96093 else {
96094 this.goToNode(node);
96095 }
96096 },
96097
96098 onBeforeSelect: function () {
96099 this.fireEvent.apply(this, [].concat('beforeselect', this, Array.prototype.slice.call(arguments)));
96100 },
96101
96102 onContainerTap: function () {
96103 this.fireEvent.apply(this, [].concat('containertap', this, Array.prototype.slice.call(arguments)));
96104 },
96105
96106 onSelectionChange: function () {
96107 this.fireEvent.apply(this, [].concat('selectionchange', this, Array.prototype.slice.call(arguments)));
96108 },
96109
96110 onItemDoubleTap: function () {
96111 this.fireEvent.apply(this, [].concat('itemdoubletap', this, Array.prototype.slice.call(arguments)));
96112 },
96113
96114 onStoreBeforeLoad: function () {
96115 var loadingText = this.getLoadingText(),
96116 scrollable = this.getScrollable();
96117
96118 if (loadingText) {
96119 this.setMasked({
96120 xtype: 'loadmask',
96121 message: loadingText
96122 });
96123
96124 //disable scrolling while it is masked
96125 if (scrollable) {
96126 scrollable.getScroller().setDisabled(true);
96127 }
96128 }
96129
96130 this.fireEvent.apply(this, [].concat('beforeload', this, Array.prototype.slice.call(arguments)));
96131 },
96132
96133 onStoreLoad: function (store, records, successful, operation) {
96134 this.setMasked(false);
96135 this.fireEvent.apply(this, [].concat('load', this, Array.prototype.slice.call(arguments)));
96136
96137 if (store.indexOf(this.getLastNode()) === -1) {
96138 this.goToNode(store.getRoot());
96139 }
96140 },
96141
96142 /**
96143 * Called when the backButton has been tapped.
96144 */
96145 onBackTap: function () {
96146 var me = this,
96147 node = me.getLastNode(),
96148 detailCard = me.getDetailCard(),
96149 detailCardActive = detailCard && me.getActiveItem() == detailCard,
96150 lastActiveList = me.getLastActiveList();
96151
96152 this.fireAction('back', [this, node, lastActiveList, detailCardActive], 'doBack');
96153 },
96154
96155 doBack: function (me, node, lastActiveList, detailCardActive) {
96156 var layout = me.getLayout(),
96157 animation = (layout) ? layout.getAnimation() : null;
96158
96159 if (detailCardActive && lastActiveList) {
96160 if (animation) {
96161 animation.setReverse(true);
96162 }
96163 me.setActiveItem(lastActiveList);
96164 me.setLastNode(node.parentNode);
96165 me.syncToolbar();
96166 }
96167 else {
96168 this.goToNode(node.parentNode);
96169 }
96170 },
96171
96172 updateData: function (data) {
96173 if (!this.getStore()) {
96174 this.setStore(new Ext.data.TreeStore({
96175 root: data
96176 }));
96177 }
96178 },
96179
96180 applyStore: function (store) {
96181 if (store) {
96182 if (Ext.isString(store)) {
96183 // store id
96184 store = Ext.data.StoreManager.get(store);
96185 } else {
96186 // store instance or store config
96187 if (!(store instanceof Ext.data.TreeStore)) {
96188 store = Ext.factory(store, Ext.data.TreeStore, null);
96189 }
96190 }
96191
96192 // <debug>
96193 if (!store) {
96194 Ext.Logger.warn("The specified Store cannot be found", this);
96195 }
96196 //</debug>
96197 }
96198
96199 return store;
96200 },
96201
96202 storeListeners: {
96203 rootchange: 'onStoreRootChange',
96204 load: 'onStoreLoad',
96205 beforeload: 'onStoreBeforeLoad'
96206 },
96207
96208 updateStore: function (newStore, oldStore) {
96209 var me = this,
96210 listeners = this.storeListeners;
96211
96212 listeners.scope = me;
96213
96214 if (oldStore && Ext.isObject(oldStore) && oldStore.isStore) {
96215 if (oldStore.autoDestroy) {
96216 oldStore.destroy();
96217 }
96218 oldStore.un(listeners);
96219 }
96220
96221 if (newStore) {
96222 newStore.on(listeners);
96223 me.goToNode(newStore.getRoot());
96224 }
96225 },
96226
96227 onStoreRootChange: function (store, node) {
96228 this.goToNode(node);
96229 },
96230
96231 applyBackButton: function (config) {
96232 return Ext.factory(config, Ext.Button, this.getBackButton());
96233 },
96234
96235 applyDetailCard: function (config, oldDetailCard) {
96236 if (config === null) {
96237 return Ext.factory(config, Ext.Component, oldDetailCard);
96238 } else {
96239 return Ext.factory(config, Ext.Component);
96240 }
96241 },
96242
96243 updateBackButton: function (newButton, oldButton) {
96244 if (newButton) {
96245 var me = this;
96246 newButton.on('tap', me.onBackTap, me);
96247 newButton.setText(me.getBackText());
96248
96249 var toolbar = me.getToolbar();
96250 if (this.$backButtonContainer) {
96251 this.$backButtonContainer.insert(0, newButton);
96252 } else {
96253 toolbar.insert(0, newButton);
96254 }
96255 }
96256 else if (oldButton) {
96257 oldButton.destroy();
96258 }
96259 },
96260
96261 applyToolbar: function (config) {
96262 if (config && config.splitNavigation) {
96263 Ext.apply(config, {
96264 docked: 'top',
96265 xtype: 'titlebar',
96266 ui: 'light'
96267 });
96268
96269 var containerConfig = (config.splitNavigation === true) ? {} : config.splitNavigation;
96270
96271 this.$backButtonContainer = this.add(Ext.apply({
96272 xtype: 'toolbar',
96273 docked: 'bottom',
96274 hidden: true,
96275 ui: 'dark'
96276 }, containerConfig));
96277 }
96278
96279 return Ext.factory(config, Ext.TitleBar, this.getToolbar());
96280 },
96281
96282 updateToolbar: function (newToolbar, oldToolbar) {
96283 var me = this;
96284 if (newToolbar) {
96285 newToolbar.setTitle(me.getTitle());
96286 if (!newToolbar.getParent()) {
96287 me.add(newToolbar);
96288 }
96289 }
96290 else if (oldToolbar) {
96291 oldToolbar.destroy();
96292 }
96293 },
96294
96295 updateUseToolbar: function (newUseToolbar, oldUseToolbar) {
96296 if (!newUseToolbar) {
96297 this.setToolbar(false);
96298 }
96299 },
96300
96301 updateTitle: function (newTitle) {
96302 var me = this,
96303 toolbar = me.getToolbar();
96304
96305 if (toolbar) {
96306 if (me.getUpdateTitleText()) {
96307 toolbar.setTitle(newTitle);
96308 }
96309 }
96310 },
96311
96312 /**
96313 * Override this method to provide custom template rendering of individual
96314 * nodes. The template will receive all data within the Record and will also
96315 * receive whether or not it is a leaf node.
96316 * @param {Ext.data.Record} node
96317 * @return {String}
96318 */
96319 getItemTextTpl: function (node) {
96320 return '{' + this.getDisplayField() + '}';
96321 },
96322
96323 /**
96324 * Override this method to provide custom template rendering of titles/back
96325 * buttons when {@link #useTitleAsBackText} is enabled.
96326 * @param {Ext.data.Record} node
96327 * @return {String}
96328 */
96329 getTitleTextTpl: function (node) {
96330 return '{' + this.getDisplayField() + '}';
96331 },
96332
96333 /**
96334 * @private
96335 */
96336 renderTitleText: function (node, forBackButton) {
96337 if (!node.titleTpl) {
96338 node.titleTpl = Ext.create('Ext.XTemplate', this.getTitleTextTpl(node));
96339 }
96340
96341 if (node.isRoot()) {
96342 var initialTitle = this.getInitialConfig('title');
96343 return (forBackButton && initialTitle === '') ? this.getInitialConfig('backText') : initialTitle;
96344 }
96345
96346 return node.titleTpl.applyTemplate(node.data);
96347 },
96348
96349 /**
96350 * Method to handle going to a specific node within this nested list. Node must be part of the
96351 * internal {@link #store}.
96352 * @param {Ext.data.NodeInterface} node The specified node to navigate to.
96353 */
96354 goToNode: function (node) {
96355 if (!node) {
96356 return;
96357 }
96358
96359 var me = this,
96360 activeItem = me.getActiveItem(),
96361 detailCard = me.getDetailCard(),
96362 detailCardActive = detailCard && me.getActiveItem() == detailCard,
96363 reverse = me.goToNodeReverseAnimation(node),
96364 firstList = me.firstList,
96365 secondList = me.secondList,
96366 layout = me.getLayout(),
96367 animation = (layout) ? layout.getAnimation() : null,
96368 list;
96369
96370 //if the node is a leaf, throw an error
96371 if (node.isLeaf()) {
96372 throw new Error('goToNode: passed a node which is a leaf.');
96373 }
96374
96375 //if we are currently at the passed node, do nothing.
96376 if (node == me.getLastNode() && !detailCardActive) {
96377 return;
96378 }
96379
96380 if (detailCardActive) {
96381 if (animation) {
96382 animation.setReverse(true);
96383 }
96384 list = me.getLastActiveList();
96385 list.getStore().setNode(node);
96386 node.expand();
96387 me.setActiveItem(list);
96388 }
96389 else {
96390 if (animation) {
96391 animation.setReverse(reverse);
96392 }
96393
96394 if (firstList && secondList) {
96395 //firstList and secondList have both been created
96396 activeItem = me.getActiveItem();
96397
96398 me.setLastActiveList(activeItem);
96399 list = (activeItem == firstList) ? secondList : firstList;
96400
96401 list.getStore().setNode(node);
96402 node.expand();
96403
96404 me.setActiveItem(list);
96405 if (this.getClearSelectionOnListChange()) {
96406 list.deselectAll();
96407 }
96408 }
96409 else if (firstList) {
96410 //only firstList has been created
96411 me.setLastActiveList(me.getActiveItem());
96412 me.setActiveItem(me.getList(node));
96413 me.secondList = me.getActiveItem();
96414 }
96415 else {
96416 //no lists have been created
96417 me.setActiveItem(me.getList(node));
96418 me.firstList = me.getActiveItem();
96419 }
96420 }
96421
96422 me.fireEvent('listchange', this, me.getActiveItem());
96423
96424 me.setLastNode(node);
96425
96426 me.syncToolbar();
96427 },
96428
96429
96430 /**
96431 * The leaf you want to navigate to. You should pass a node instance.
96432 * @param {Ext.data.NodeInterface} node The specified node to navigate to.
96433 */
96434 goToLeaf: function (node) {
96435 if (!node.isLeaf()) {
96436 throw new Error('goToLeaf: passed a node which is not a leaf.');
96437 }
96438
96439 var me = this,
96440 card = me.getDetailCard(node),
96441 container = me.getDetailContainer(),
96442 sharedContainer = container == this,
96443 layout = me.getLayout(),
96444 animation = (layout) ? layout.getAnimation() : false;
96445
96446 if (card) {
96447 if (container.getItems().indexOf(card) === -1) {
96448 container.add(card);
96449 }
96450 if (sharedContainer) {
96451 if (me.getActiveItem() instanceof Ext.dataview.List) {
96452 me.setLastActiveList(me.getActiveItem());
96453 }
96454 me.setLastNode(node);
96455 }
96456 if (animation) {
96457 animation.setReverse(false);
96458 }
96459 container.setActiveItem(card);
96460 me.syncToolbar();
96461 }
96462 },
96463
96464 /**
96465 * @private
96466 * Method which updates the {@link #backButton} and {@link #toolbar} with the latest information from
96467 * the current node.
96468 */
96469 syncToolbar: function (forceDetail) {
96470 var me = this,
96471 detailCard = me.getDetailCard(),
96472 node = me.getLastNode(),
96473 detailActive = forceDetail || (detailCard && (me.getActiveItem() == detailCard)),
96474 parentNode = (detailActive) ? node : node.parentNode,
96475 backButton = me.getBackButton();
96476
96477 //show/hide the backButton, and update the backButton text, if one exists
96478 if (backButton) {
96479 var toolbar = this.getToolbar(),
96480 splitNavigation = toolbar.getInitialConfig("splitNavigation");
96481
96482 if (splitNavigation) {
96483 this.$backButtonContainer[parentNode ? 'show' : 'hide']();
96484 }
96485
96486 backButton[parentNode ? 'show' : 'hide']();
96487 if (parentNode && me.getUseTitleAsBackText()) {
96488 backButton.setText(me.renderTitleText(node.parentNode, true));
96489 }
96490 }
96491
96492 if (node) {
96493 me.setTitle(me.renderTitleText(node));
96494 }
96495 },
96496
96497 updateBackText: function (newText) {
96498 this.getBackButton().setText(newText);
96499 },
96500
96501 /**
96502 * @private
96503 * Returns `true` if the passed node should have a reverse animation from the previous current node.
96504 * @param {Ext.data.NodeInterface} node
96505 */
96506 goToNodeReverseAnimation: function (node) {
96507 var me = this,
96508 lastNode = me.getLastNode();
96509 if (!lastNode) {
96510 return false;
96511 }
96512
96513 return (!lastNode.contains(node) && lastNode.isAncestor(node)) ? true : false;
96514 },
96515
96516 /**
96517 * @private
96518 * Returns the list config for a specified node.
96519 * @param {HTMLElement} node The node for the list config.
96520 */
96521 getList: function (node) {
96522 var me = this,
96523 nodeStore = Ext.create('Ext.data.NodeStore', {
96524 recursive: false,
96525 node: node,
96526 rootVisible: false,
96527 model: me.getStore().getModel()
96528 });
96529
96530 node.expand();
96531
96532 return Ext.Object.merge({
96533 xtype: 'list',
96534 useSimpleItems: me.getUseSimpleItems(),
96535 pressedDelay: 250,
96536 autoDestroy: true,
96537 store: nodeStore,
96538 onItemDisclosure: me.getOnItemDisclosure(),
96539 allowDeselect: me.getAllowDeselect(),
96540 itemHeight: me.getItemHeight(),
96541 variableHeights: me.getVariableHeights(),
96542 emptyText: me.getEmptyText(),
96543 listeners: [
96544 { event: 'itemdoubletap', fn: 'onItemDoubleTap', scope: me },
96545 { event: 'itemtap', fn: 'onItemInteraction', scope: me, order: 'before'},
96546 { event: 'itemtouchstart', fn: 'onItemInteraction', scope: me, order: 'before'},
96547 { event: 'itemtap', fn: 'onItemTap', scope: me },
96548 { event: 'beforeselectionchange', fn: 'onBeforeSelect', scope: me },
96549 { event: 'containertap', fn: 'onContainerTap', scope: me },
96550 { event: 'selectionchange', fn: 'onSelectionChange', order: 'before', scope: me }
96551 ],
96552 itemTpl: '<span<tpl if="leaf == true"> class="x-list-item-leaf"</tpl>>' + me.getItemTextTpl(node) + '</span>'
96553 }, this.getListConfig());
96554 }
96555
96556 }, function () {
96557 });
96558
96559
96560 /**
96561 * @private
96562 */
96563 Ext.define('Ext.dataview.element.List', {
96564 extend: Ext.dataview.element.Container ,
96565
96566 updateBaseCls: function(newBaseCls) {
96567 var me = this;
96568
96569 me.itemClsShortCache = newBaseCls + '-item';
96570
96571 me.headerClsShortCache = newBaseCls + '-header';
96572 me.headerClsCache = '.' + me.headerClsShortCache;
96573
96574 me.headerItemClsShortCache = newBaseCls + '-header-item';
96575
96576 me.footerClsShortCache = newBaseCls + '-footer-item';
96577 me.footerClsCache = '.' + me.footerClsShortCache;
96578
96579 me.labelClsShortCache = newBaseCls + '-item-label';
96580 me.labelClsCache = '.' + me.labelClsShortCache;
96581
96582 me.disclosureClsShortCache = newBaseCls + '-disclosure';
96583 me.disclosureClsCache = '.' + me.disclosureClsShortCache;
96584
96585 me.iconClsShortCache = newBaseCls + '-icon';
96586 me.iconClsCache = '.' + me.iconClsShortCache;
96587
96588 this.callParent(arguments);
96589 },
96590
96591 hiddenDisplayCache: Ext.baseCSSPrefix + 'hidden-display',
96592
96593 getItemElementConfig: function(index, data) {
96594 var me = this,
96595 dataview = me.dataview,
96596 itemCls = dataview.getItemCls(),
96597 cls = me.itemClsShortCache,
96598 config, iconSrc;
96599
96600 if (itemCls) {
96601 cls += ' ' + itemCls;
96602 }
96603
96604 config = {
96605 cls: cls,
96606 children: [{
96607 cls: me.labelClsShortCache,
96608 html: dataview.getItemTpl().apply(data)
96609 }]
96610 };
96611
96612 if (dataview.getIcon()) {
96613 iconSrc = data.iconSrc;
96614 config.children.push({
96615 cls: me.iconClsShortCache,
96616 style: 'background-image: ' + iconSrc ? 'url("' + newSrc + '")' : ''
96617 });
96618 }
96619
96620 if (dataview.getOnItemDisclosure()) {
96621 config.children.push({
96622 cls: me.disclosureClsShortCache + ' ' + ((data[dataview.getDisclosureProperty()] === false) ? me.hiddenDisplayCache : '')
96623 });
96624 }
96625 return config;
96626 },
96627
96628 updateListItem: function(record, item) {
96629 var me = this,
96630 dataview = me.dataview,
96631 extItem = Ext.fly(item),
96632 innerItem = extItem.down(me.labelClsCache, true),
96633 data = dataview.prepareData(record.getData(true), dataview.getStore().indexOf(record), record),
96634 disclosureProperty = dataview.getDisclosureProperty(),
96635 hasDisclosureProperty = data && data.hasOwnProperty(disclosureProperty),
96636 iconSrc = data && data.hasOwnProperty('iconSrc'),
96637 disclosureEl, iconEl;
96638
96639 innerItem.innerHTML = dataview.getItemTpl().apply(data);
96640
96641 if (hasDisclosureProperty) {
96642 disclosureEl = extItem.down(me.disclosureClsCache);
96643 disclosureEl[data[disclosureProperty] === false ? 'addCls' : 'removeCls'](me.hiddenDisplayCache);
96644 }
96645
96646 if (dataview.getIcon()) {
96647 iconEl = extItem.down(me.iconClsCache, true);
96648 iconEl.style.backgroundImage = iconSrc ? 'url("' + iconSrc + '")' : '';
96649 }
96650 },
96651
96652 doRemoveHeaders: function() {
96653 var me = this,
96654 headerClsShortCache = me.headerItemClsShortCache,
96655 existingHeaders = me.element.query(me.headerClsCache),
96656 existingHeadersLn = existingHeaders.length,
96657 i = 0,
96658 item;
96659
96660 for (; i < existingHeadersLn; i++) {
96661 item = existingHeaders[i];
96662 Ext.fly(item.parentNode).removeCls(headerClsShortCache);
96663 Ext.get(item).destroy();
96664 }
96665 },
96666
96667 doRemoveFooterCls: function() {
96668 var me = this,
96669 footerClsShortCache = me.footerClsShortCache,
96670 existingFooters = me.element.query(me.footerClsCache),
96671 existingFootersLn = existingFooters.length,
96672 i = 0;
96673
96674 for (; i < existingFootersLn; i++) {
96675 Ext.fly(existingFooters[i]).removeCls(footerClsShortCache);
96676 }
96677 },
96678
96679 doAddHeader: function(item, html) {
96680 item = Ext.fly(item);
96681 if (html) {
96682 item.insertFirst(Ext.Element.create({
96683 cls: this.headerClsShortCache,
96684 html: html
96685 }));
96686 }
96687 item.addCls(this.headerItemClsShortCache);
96688 },
96689
96690 destroy: function() {
96691 this.doRemoveHeaders();
96692 this.callParent();
96693 }
96694 });
96695
96696 /**
96697 * @private
96698 */
96699 Ext.define('Ext.util.Audio', {
96700 singleton: true,
96701 ctx: null,
96702
96703 beep: function(callback) {
96704 this.oscillate(200, 1, callback);
96705 },
96706
96707 oscillate: function(duration, type, callback) {
96708 if (!this.ctx) {
96709 this.ctx = new (window.audioContext || window.webkitAudioContext);
96710 }
96711
96712 if (!this.ctx) {
96713 console.log("BEEP");
96714 return;
96715 }
96716
96717 type = (type % 5) || 0;
96718
96719 try {
96720 var osc = this.ctx.createOscillator();
96721 osc.type = type;
96722 osc.connect(this.ctx.destination);
96723 osc.noteOn(0);
96724
96725 setTimeout(function() {
96726 osc.noteOff(0);
96727 if(callback) callback();
96728 }, duration);
96729 } catch (e) {
96730 throw new Error("[Ext.util.Audio.oscillate] Error with Oscillator playback");
96731 }
96732
96733 }
96734
96735 })
96736 ;
96737
96738
96739
96740
96741 /**
96742 * @class Ext.direct.Event
96743 * A base class for all Ext.direct events. An event is
96744 * created after some kind of interaction with the server.
96745 * The event class is essentially just a data structure
96746 * to hold a Direct response.
96747 */
96748 Ext.define('Ext.direct.Event', {
96749 alias: 'direct.event',
96750
96751
96752
96753 config: {
96754 status: true,
96755
96756 /**
96757 * @cfg {Object} data The raw data for this event.
96758 * @accessor
96759 */
96760 data: null,
96761
96762 /**
96763 * @cfg {String} name The name of this Event.
96764 * @accessor
96765 */
96766 name: 'event',
96767
96768 xhr: null,
96769
96770 code: null,
96771
96772 message: '',
96773
96774 result: null,
96775
96776 transaction: null
96777 },
96778
96779 constructor: function(config) {
96780 this.initConfig(config)
96781 }
96782 });
96783
96784 /**
96785 * @class Ext.direct.RemotingEvent
96786 * An event that is fired when data is received from a
96787 * {@link Ext.direct.RemotingProvider}. Contains a method to the
96788 * related transaction for the direct request, see {@link #getTransaction}
96789 */
96790 Ext.define('Ext.direct.RemotingEvent', {
96791 extend: Ext.direct.Event ,
96792
96793 alias: 'direct.rpc',
96794
96795 config: {
96796 name: 'remoting',
96797 tid: null,
96798 transaction: null
96799 },
96800
96801 /**
96802 * Get the transaction associated with this event.
96803 * @return {Ext.direct.Transaction} The transaction
96804 */
96805 getTransaction: function() {
96806 return this._transaction || Ext.direct.Manager.getTransaction(this.getTid());
96807 }
96808 });
96809
96810 /**
96811 * @class Ext.direct.ExceptionEvent
96812 * An event that is fired when an exception is received from a {@link Ext.direct.RemotingProvider}
96813 */
96814 Ext.define('Ext.direct.ExceptionEvent', {
96815
96816 extend: Ext.direct.RemotingEvent ,
96817
96818 alias: 'direct.exception',
96819
96820 config: {
96821 status: false,
96822 name: 'exception',
96823 error: null
96824 }
96825 });
96826
96827 /**
96828 * Ext.direct.Provider is an abstract class meant to be extended.
96829 *
96830 * For example Ext JS implements the following subclasses:
96831 *
96832 * Provider
96833 * |
96834 * +---{@link Ext.direct.JsonProvider JsonProvider}
96835 * |
96836 * +---{@link Ext.direct.PollingProvider PollingProvider}
96837 * |
96838 * +---{@link Ext.direct.RemotingProvider RemotingProvider}
96839 *
96840 * @abstract
96841 */
96842 Ext.define('Ext.direct.Provider', {
96843 alias: 'direct.provider',
96844
96845 mixins: {
96846 observable: Ext.mixin.Observable
96847 },
96848
96849 config: {
96850 /**
96851 * @cfg {String} id
96852 * The unique id of the provider (defaults to an auto-assigned id).
96853 * You should assign an id if you need to be able to access the provider later and you do
96854 * not have an object reference available, for example:
96855 *
96856 * Ext.direct.Manager.addProvider({
96857 * type: 'polling',
96858 * url: 'php/poll.php',
96859 * id: 'poll-provider'
96860 * });
96861 * var p = {@link Ext.direct.Manager}.{@link Ext.direct.Manager#getProvider getProvider}('poll-provider');
96862 * p.disconnect();
96863 *
96864 */
96865 id: undefined
96866 },
96867
96868 /**
96869 * @event connect
96870 * Fires when the Provider connects to the server-side
96871 * @param {Ext.direct.Provider} provider The {@link Ext.direct.Provider Provider}.
96872 */
96873
96874 /**
96875 * @event disconnect
96876 * Fires when the Provider disconnects from the server-side
96877 * @param {Ext.direct.Provider} provider The {@link Ext.direct.Provider Provider}.
96878 */
96879
96880 /**
96881 * @event data
96882 * Fires when the Provider receives data from the server-side
96883 * @param {Ext.direct.Provider} provider The {@link Ext.direct.Provider Provider}.
96884 * @param {Ext.direct.Event} e The Ext.direct.Event type that occurred.
96885 */
96886
96887 /**
96888 * @event exception
96889 * Fires when the Provider receives an exception from the server-side
96890 */
96891
96892 /**
96893 * @property {Boolean} isProvider Signifies this instance is an Ext.Direct provider.
96894 */
96895 isProvider : true,
96896
96897 constructor : function(config){
96898 this.initConfig(config);
96899 },
96900
96901 applyId: function(id) {
96902 if (id === undefined) {
96903 id = this.getUniqueId();
96904 }
96905 return id;
96906 },
96907
96908 /**
96909 * Returns whether or not the server-side is currently connected.
96910 * Abstract method for subclasses to implement.
96911 * @return {Boolean}
96912 */
96913 isConnected: function() {
96914 return false;
96915 },
96916
96917 /**
96918 * Abstract methods for subclasses to implement.
96919 * @method
96920 */
96921 connect: Ext.emptyFn,
96922
96923 /**
96924 * Abstract methods for subclasses to implement.
96925 * @method
96926 */
96927 disconnect: Ext.emptyFn
96928 });
96929
96930 /**
96931 * @class Ext.direct.JsonProvider
96932 *
96933 * A base provider for communicating using JSON. This is an abstract class
96934 * and should not be instanced directly.
96935 * @abstract
96936 */
96937
96938 Ext.define('Ext.direct.JsonProvider', {
96939 extend: Ext.direct.Provider ,
96940
96941 alias: 'direct.jsonprovider',
96942
96943
96944
96945 /**
96946 * Parse the JSON response.
96947 * @private
96948 * @param {Object} response The XHR response object.
96949 * @return {Object} The data in the response.
96950 */
96951 parseResponse: function(response) {
96952 if (!Ext.isEmpty(response.responseText)) {
96953 if (Ext.isObject(response.responseText)) {
96954 return response.responseText;
96955 }
96956 return Ext.decode(response.responseText);
96957 }
96958 return null;
96959 },
96960
96961 /**
96962 * Creates a set of events based on the XHR response.
96963 * @private
96964 * @param {Object} response The XHR response.
96965 * @return {Ext.direct.Event[]} An array of {@link Ext.direct.Event} objects.
96966 */
96967 createEvents: function(response) {
96968 var data = null,
96969 events = [],
96970 i = 0,
96971 ln, event;
96972
96973 try {
96974 data = this.parseResponse(response);
96975 } catch(e) {
96976 event = Ext.create('Ext.direct.ExceptionEvent', {
96977 data: e,
96978 xhr: response,
96979 code: Ext.direct.Manager.exceptions.PARSE,
96980 message: 'Error parsing json response: \n\n ' + data
96981 });
96982 return [event];
96983 }
96984
96985 if (Ext.isArray(data)) {
96986 for (ln = data.length; i < ln; ++i) {
96987 events.push(this.createEvent(data[i]));
96988 }
96989 } else {
96990 events.push(this.createEvent(data));
96991 }
96992 return events;
96993 },
96994
96995 /**
96996 * Create an event from a response object.
96997 * @param {Object} response The XHR response object.
96998 * @return {Ext.direct.Event} The event.
96999 */
97000 createEvent: function(response) {
97001 return Ext.create('direct.' + response.type, response);
97002 }
97003 });
97004
97005 /**
97006 * The DelayedTask class provides a convenient way to "buffer" the execution of a method,
97007 * performing `setTimeout` where a new timeout cancels the old timeout. When called, the
97008 * task will wait the specified time period before executing. If during that time period,
97009 * the task is called again, the original call will be canceled. This continues so that
97010 * the function is only called a single time for each iteration.
97011 *
97012 * This method is especially useful for things like detecting whether a user has finished
97013 * typing in a text field. An example would be performing validation on a keypress. You can
97014 * use this class to buffer the keypress events for a certain number of milliseconds, and
97015 * perform only if they stop for that amount of time.
97016 *
97017 * Using {@link Ext.util.DelayedTask} is very simple:
97018 *
97019 * //create the delayed task instance with our callback
97020 * var task = Ext.create('Ext.util.DelayedTask', {
97021 * fn: function() {
97022 * console.log('callback!');
97023 * }
97024 * });
97025 *
97026 * task.delay(1500); //the callback function will now be called after 1500ms
97027 *
97028 * task.cancel(); //the callback function will never be called now, unless we call delay() again
97029 *
97030 * ## Example
97031 *
97032 * @example
97033 * //create a textfield where we can listen to text
97034 * var field = Ext.create('Ext.field.Text', {
97035 * xtype: 'textfield',
97036 * label: 'Length: 0'
97037 * });
97038 *
97039 * //add the textfield into a fieldset
97040 * Ext.Viewport.add({
97041 * xtype: 'formpanel',
97042 * items: [{
97043 * xtype: 'fieldset',
97044 * items: [field],
97045 * instructions: 'Type into the field and watch the count go up after 500ms.'
97046 * }]
97047 * });
97048 *
97049 * //create our delayed task with a function that returns the fields length as the fields label
97050 * var task = Ext.create('Ext.util.DelayedTask', function() {
97051 * field.setLabel('Length: ' + field.getValue().length);
97052 * });
97053 *
97054 * // Wait 500ms before calling our function. If the user presses another key
97055 * // during that 500ms, it will be canceled and we'll wait another 500ms.
97056 * field.on('keyup', function() {
97057 * task.delay(500);
97058 * });
97059 *
97060 * @constructor
97061 * The parameters to this constructor serve as defaults and are not required.
97062 * @param {Function} fn The default function to call.
97063 * @param {Object} scope The default scope (The `this` reference) in which the function is called. If
97064 * not specified, `this` will refer to the browser window.
97065 * @param {Array} args The default Array of arguments.
97066 */
97067 Ext.define('Ext.util.DelayedTask', {
97068 config: {
97069 interval: null,
97070 delay: null,
97071 fn: null,
97072 scope: null,
97073 args: null
97074 },
97075
97076 constructor: function(fn, scope, args) {
97077 var config = {
97078 fn: fn,
97079 scope: scope,
97080 args: args
97081 };
97082
97083 this.initConfig(config);
97084 },
97085
97086 /**
97087 * Cancels any pending timeout and queues a new one.
97088 * @param {Number} delay The milliseconds to delay
97089 * @param {Function} newFn Overrides the original function passed when instantiated.
97090 * @param {Object} newScope Overrides the original `scope` passed when instantiated. Remember that if no scope
97091 * is specified, `this` will refer to the browser window.
97092 * @param {Array} newArgs Overrides the original `args` passed when instantiated.
97093 */
97094 delay: function(delay, newFn, newScope, newArgs) {
97095 var me = this;
97096
97097 //cancel any existing queued functions
97098 me.cancel();
97099
97100 //set all the new configurations
97101
97102 if (Ext.isNumber(delay)) {
97103 me.setDelay(delay);
97104 }
97105
97106 if (Ext.isFunction(newFn)) {
97107 me.setFn(newFn);
97108 }
97109
97110 if (newScope) {
97111 me.setScope(newScope);
97112 }
97113
97114 if (newScope) {
97115 me.setArgs(newArgs);
97116 }
97117
97118 //create the callback method for this delayed task
97119 var call = function() {
97120 me.getFn().apply(me.getScope(), me.getArgs() || []);
97121 me.cancel();
97122 };
97123
97124 me.setInterval(setInterval(call, me.getDelay()));
97125 },
97126
97127 /**
97128 * Cancel the last queued timeout
97129 */
97130 cancel: function() {
97131 this.setInterval(null);
97132 },
97133
97134 /**
97135 * @private
97136 * Clears the old interval
97137 */
97138 updateInterval: function(newInterval, oldInterval) {
97139 if (oldInterval) {
97140 clearInterval(oldInterval);
97141 }
97142 },
97143
97144 /**
97145 * @private
97146 * Changes the value into an array if it isn't one.
97147 */
97148 applyArgs: function(config) {
97149 if (!Ext.isArray(config)) {
97150 config = [config];
97151 }
97152
97153 return config;
97154 }
97155 });
97156
97157 /**
97158 * @class Ext.direct.PollingProvider
97159 *
97160 * Provides for repetitive polling of the server at distinct {@link #interval intervals}.
97161 * The initial request for data originates from the client, and then is responded to by the
97162 * server.
97163 *
97164 * All configurations for the PollingProvider should be generated by the server-side
97165 * API portion of the Ext.Direct stack.
97166 *
97167 * An instance of PollingProvider may be created directly via the new keyword or by simply
97168 * specifying `type = 'polling'`. For example:
97169 *
97170 * var pollA = Ext.create('Ext.direct.PollingProvider', {
97171 * type:'polling',
97172 * url: 'php/pollA.php'
97173 * });
97174 *
97175 * Ext.direct.Manager.addProvider(pollA);
97176 * pollA.disconnect();
97177 *
97178 * Ext.direct.Manager.addProvider({
97179 * type:'polling',
97180 * url: 'php/pollB.php',
97181 * id: 'pollB-provider'
97182 * });
97183 *
97184 * var pollB = Ext.direct.Manager.getProvider('pollB-provider');
97185 */
97186 Ext.define('Ext.direct.PollingProvider', {
97187 extend: Ext.direct.JsonProvider ,
97188 alias: 'direct.pollingprovider',
97189
97190
97191
97192
97193
97194 config: {
97195 /**
97196 * @cfg {Number} interval
97197 * How often to poll the server-side, in milliseconds.
97198 */
97199 interval: 3000,
97200
97201 /**
97202 * @cfg {Object} baseParams
97203 * An object containing properties which are to be sent as parameters on every polling request.
97204 */
97205 baseParams: null,
97206
97207 /**
97208 * @cfg {String/Function} url
97209 * The url which the PollingProvider should contact with each request. This can also be
97210 * an imported {@link Ext.Direct} method which will accept the `{@link #baseParams}` as its only argument.
97211 */
97212 url: null
97213 },
97214
97215 /**
97216 * @event beforepoll
97217 * Fired immediately before a poll takes place, an event handler can return `false`
97218 * in order to cancel the poll.
97219 * @param {Ext.direct.PollingProvider} this
97220 */
97221
97222 /**
97223 * @event poll
97224 * This event has not yet been implemented.
97225 * @param {Ext.direct.PollingProvider} this
97226 */
97227
97228 /**
97229 * @inheritdoc
97230 */
97231 isConnected: function() {
97232 return !!this.pollTask;
97233 },
97234
97235 /**
97236 * Connect to the server-side and begin the polling process. To handle each
97237 * response subscribe to the `data` event.
97238 */
97239 connect: function() {
97240 var me = this,
97241 url = me.getUrl(),
97242 baseParams = me.getBaseParams();
97243
97244 if (url && !me.pollTask) {
97245 me.pollTask = setInterval(function() {
97246 if (me.fireEvent('beforepoll', me) !== false) {
97247 if (Ext.isFunction(url)) {
97248 url(baseParams);
97249 } else {
97250 Ext.Ajax.request({
97251 url: url,
97252 callback: me.onData,
97253 scope: me,
97254 params: baseParams
97255 });
97256 }
97257 }
97258 }, me.getInterval());
97259 me.fireEvent('connect', me);
97260 } else if (!url) {
97261 //<debug>
97262 Ext.Error.raise('Error initializing PollingProvider, no url configured.');
97263 //</debug>
97264 }
97265 },
97266
97267 /**
97268 * Disconnect from the server-side and stop the polling process. The `disconnect`
97269 * event will be fired on a successful disconnect.
97270 */
97271 disconnect: function() {
97272 var me = this;
97273
97274 if (me.pollTask) {
97275 clearInterval(me.pollTask);
97276 delete me.pollTask;
97277 me.fireEvent('disconnect', me);
97278 }
97279 },
97280
97281 // @private
97282 onData: function(opt, success, response) {
97283 var me = this,
97284 i = 0,
97285 len,
97286 events;
97287
97288 if (success) {
97289 events = me.createEvents(response);
97290 for (len = events.length; i < len; ++i) {
97291 me.fireEvent('data', me, events[i]);
97292 }
97293 } else {
97294 me.fireEvent('data', me, Ext.create('Ext.direct.ExceptionEvent', {
97295 data: null,
97296 code: Ext.direct.Manager.exceptions.TRANSPORT,
97297 message: 'Unable to connect to the server.',
97298 xhr: response
97299 }));
97300 }
97301 }
97302 });
97303
97304 /**
97305 * Small utility class used internally to represent a Direct method.
97306 * @class Ext.direct.RemotingMethod
97307 * @private
97308 */
97309 Ext.define('Ext.direct.RemotingMethod', {
97310 config: {
97311 name: null,
97312 params: null,
97313 formHandler: null,
97314 len: null,
97315 ordered: true
97316 },
97317
97318 constructor: function(config) {
97319 this.initConfig(config);
97320 },
97321
97322 applyParams: function(params) {
97323 if (Ext.isNumber(params)) {
97324 this.setLen(params);
97325 } else if (Ext.isArray(params)) {
97326 this.setOrdered(false);
97327
97328 var ln = params.length,
97329 ret = [],
97330 i, param, name;
97331
97332 for (i = 0; i < ln; i++) {
97333 param = params[i];
97334 name = Ext.isObject(param) ? param.name : param;
97335 ret.push(name);
97336 }
97337
97338 return ret;
97339 }
97340 },
97341
97342 getArgs: function(params, paramOrder, paramsAsHash) {
97343 var args = [],
97344 i, ln;
97345
97346 if (this.getOrdered()) {
97347 if (this.getLen() > 0) {
97348 // If a paramOrder was specified, add the params into the argument list in that order.
97349 if (paramOrder) {
97350 for (i = 0, ln = paramOrder.length; i < ln; i++) {
97351 args.push(params[paramOrder[i]]);
97352 }
97353 } else if (paramsAsHash) {
97354 // If paramsAsHash was specified, add all the params as a single object argument.
97355 args.push(params);
97356 }
97357 }
97358 } else {
97359 args.push(params);
97360 }
97361
97362 return args;
97363 },
97364
97365 /**
97366 * Takes the arguments for the Direct function and splits the arguments
97367 * from the scope and the callback.
97368 * @param {Array} args The arguments passed to the direct call
97369 * @return {Object} An object with 3 properties, args, callback & scope.
97370 */
97371 getCallData: function(args) {
97372 var me = this,
97373 data = null,
97374 len = me.getLen(),
97375 params = me.getParams(),
97376 callback, scope, name;
97377
97378 if (me.getOrdered()) {
97379 callback = args[len];
97380 scope = args[len + 1];
97381 if (len !== 0) {
97382 data = args.slice(0, len);
97383 }
97384 } else {
97385 data = Ext.apply({}, args[0]);
97386 callback = args[1];
97387 scope = args[2];
97388
97389 for (name in data) {
97390 if (data.hasOwnProperty(name)) {
97391 if (!Ext.Array.contains(params, name)) {
97392 delete data[name];
97393 }
97394 }
97395 }
97396 }
97397
97398 return {
97399 data: data,
97400 callback: callback,
97401 scope: scope
97402 };
97403 }
97404 });
97405
97406 /**
97407 * Supporting Class for Ext.Direct (not intended to be used directly).
97408 */
97409 Ext.define('Ext.direct.Transaction', {
97410 alias: 'direct.transaction',
97411 alternateClassName: 'Ext.Direct.Transaction',
97412
97413 statics: {
97414 TRANSACTION_ID: 0
97415 },
97416
97417 config: {
97418 id: undefined,
97419 provider: null,
97420 retryCount: 0,
97421 args: null,
97422 action: null,
97423 method: null,
97424 data: null,
97425 callback: null,
97426 form: null
97427 },
97428
97429 constructor: function(config) {
97430 this.initConfig(config);
97431 },
97432
97433 applyId: function(id) {
97434 if (id === undefined) {
97435 id = ++this.self.TRANSACTION_ID;
97436 }
97437 return id;
97438 },
97439
97440 updateId: function(id) {
97441 this.id = this.tid = id;
97442 },
97443
97444 getTid: function() {
97445 return this.tid;
97446 },
97447
97448 send: function(){
97449 this.getProvider().queueTransaction(this);
97450 },
97451
97452 retry: function(){
97453 this.setRetryCount(this.getRetryCount() + 1);
97454 this.send();
97455 }
97456 });
97457
97458 /**
97459 * @class Ext.direct.RemotingProvider
97460 *
97461 * The {@link Ext.direct.RemotingProvider RemotingProvider} exposes access to
97462 * server side methods on the client (a remote procedure call (RPC) type of
97463 * connection where the client can initiate a procedure on the server).
97464 *
97465 * This allows for code to be organized in a fashion that is maintainable,
97466 * while providing a clear path between client and server, something that is
97467 * not always apparent when using URLs.
97468 *
97469 * To accomplish this the server-side needs to describe what classes and methods
97470 * are available on the client-side. This configuration will typically be
97471 * outputted by the server-side Ext.Direct stack when the API description is built.
97472 */
97473 Ext.define('Ext.direct.RemotingProvider', {
97474 alias: 'direct.remotingprovider',
97475
97476 extend: Ext.direct.JsonProvider ,
97477
97478
97479
97480
97481
97482
97483
97484
97485 config: {
97486 /**
97487 * @cfg {String/Object} namespace
97488 * Namespace for the Remoting Provider (defaults to the browser global scope of _window_).
97489 * Explicitly specify the namespace Object, or specify a String to have a
97490 * {@link Ext#namespace namespace created} implicitly.
97491 */
97492 namespace: undefined,
97493
97494 /**
97495 * @cfg {String} url (required) The url to connect to the {@link Ext.direct.Manager} server-side router.
97496 */
97497 url: null,
97498
97499 /**
97500 * @cfg {String} enableUrlEncode
97501 * Specify which param will hold the arguments for the method.
97502 */
97503 enableUrlEncode: null,
97504
97505 /**
97506 * @cfg {Number/Boolean} enableBuffer
97507 *
97508 * `true` or `false` to enable or disable combining of method
97509 * calls. If a number is specified this is the amount of time in milliseconds
97510 * to wait before sending a batched request.
97511 *
97512 * Calls which are received within the specified timeframe will be
97513 * concatenated together and sent in a single request, optimizing the
97514 * application by reducing the amount of round trips that have to be made
97515 * to the server.
97516 */
97517 enableBuffer: 10,
97518
97519 /**
97520 * @cfg {Number} maxRetries
97521 * Number of times to re-attempt delivery on failure of a call.
97522 */
97523 maxRetries: 1,
97524
97525 /**
97526 * @cfg {Number} timeout
97527 * The timeout to use for each request.
97528 */
97529 timeout: undefined,
97530
97531 /**
97532 * @cfg {Object} actions
97533 * Object literal defining the server side actions and methods. For example, if
97534 * the Provider is configured with:
97535 *
97536 * actions: { // each property within the 'actions' object represents a server side Class
97537 * // array of methods within each server side Class to be stubbed out on client
97538 * TestAction: [{
97539 * name: "doEcho",
97540 * len: 1
97541 * }, {
97542 * "name": "multiply", // name of method
97543 * "len": 2 // The number of parameters that will be used to create an
97544 * // array of data to send to the server side function.
97545 * // Ensure the server sends back a Number, not a String.
97546 * }, {
97547 * name: "doForm",
97548 * formHandler: true, // direct the client to use specialized form handling method
97549 * len: 1
97550 * }]
97551 * }
97552 *
97553 * __Note:__ A Store is not required, a server method can be called at any time.
97554 * In the following example a **client side** handler is used to call the
97555 * server side method "multiply" in the server-side "TestAction" Class:
97556 *
97557 * TestAction.multiply(
97558 * 2, 4, // pass two arguments to server, so specify len=2
97559 * // callback function after the server is called
97560 * // result: the result returned by the server
97561 * // e: Ext.direct.RemotingEvent object
97562 * function(result, e) {
97563 * var t = e.getTransaction();
97564 * var action = t.action; // server side Class called
97565 * var method = t.method; // server side method called
97566 * if (e.getStatus()) {
97567 * var answer = Ext.encode(result); // 8
97568 * } else {
97569 * var msg = e.getMessage(); // failure message
97570 * }
97571 * }
97572 * );
97573 *
97574 * In the example above, the server side "multiply" function will be passed two
97575 * arguments (2 and 4). The "multiply" method should return the value 8 which will be
97576 * available as the `result` in the example above.
97577 */
97578 actions: {}
97579 },
97580
97581 /**
97582 * @event beforecall
97583 * Fires immediately before the client-side sends off the RPC call.
97584 * By returning `false` from an event handler you can prevent the call from
97585 * executing.
97586 * @param {Ext.direct.RemotingProvider} provider
97587 * @param {Ext.direct.Transaction} transaction
97588 * @param {Object} meta The meta data.
97589 */
97590
97591 /**
97592 * @event call
97593 * Fires immediately after the request to the server-side is sent. This does
97594 * NOT fire after the response has come back from the call.
97595 * @param {Ext.direct.RemotingProvider} provider
97596 * @param {Ext.direct.Transaction} transaction
97597 * @param {Object} meta The meta data.
97598 */
97599
97600 constructor : function(config) {
97601 var me = this;
97602
97603 me.callParent(arguments);
97604
97605 me.transactions = Ext.create('Ext.util.Collection', function(item) {
97606 return item.getId();
97607 });
97608 me.callBuffer = [];
97609 },
97610
97611 applyNamespace: function(namespace) {
97612 if (Ext.isString(namespace)) {
97613 return Ext.ns(namespace);
97614 }
97615 return namespace || window;
97616 },
97617
97618 /**
97619 * Initialize the API
97620 * @private
97621 */
97622 initAPI : function() {
97623 var actions = this.getActions(),
97624 namespace = this.getNamespace(),
97625 action, cls, methods,
97626 i, ln, method;
97627
97628 for (action in actions) {
97629 if (actions.hasOwnProperty(action)) {
97630 cls = namespace[action];
97631 if (!cls) {
97632 cls = namespace[action] = {};
97633 }
97634 methods = actions[action];
97635
97636 for (i = 0, ln = methods.length; i < ln; ++i) {
97637 method = Ext.create('Ext.direct.RemotingMethod', methods[i]);
97638 cls[method.getName()] = this.createHandler(action, method);
97639 }
97640 }
97641 }
97642 },
97643
97644 /**
97645 * Create a handler function for a direct call.
97646 * @private
97647 * @param {String} action The action the call is for.
97648 * @param {Object} method The details of the method.
97649 * @return {Function} A JavaScript function that will kick off the call.
97650 */
97651 createHandler : function(action, method) {
97652 var me = this,
97653 handler;
97654
97655 if (!method.getFormHandler()) {
97656 handler = function() {
97657 me.configureRequest(action, method, Array.prototype.slice.call(arguments, 0));
97658 };
97659 } else {
97660 handler = function(form, callback, scope) {
97661 me.configureFormRequest(action, method, form, callback, scope);
97662 };
97663 }
97664 handler.directCfg = {
97665 action: action,
97666 method: method
97667 };
97668 return handler;
97669 },
97670
97671 // @inheritdoc
97672 isConnected: function() {
97673 return !!this.connected;
97674 },
97675
97676 // @inheritdoc
97677 connect: function() {
97678 var me = this;
97679
97680 if (me.getUrl()) {
97681 me.initAPI();
97682 me.connected = true;
97683 me.fireEvent('connect', me);
97684 } else {
97685 //<debug>
97686 Ext.Error.raise('Error initializing RemotingProvider, no url configured.');
97687 //</debug>
97688 }
97689 },
97690
97691 // @inheritdoc
97692 disconnect: function() {
97693 var me = this;
97694
97695 if (me.connected) {
97696 me.connected = false;
97697 me.fireEvent('disconnect', me);
97698 }
97699 },
97700
97701 /**
97702 * Run any callbacks related to the transaction.
97703 * @private
97704 * @param {Ext.direct.Transaction} transaction The transaction
97705 * @param {Ext.direct.Event} event The event
97706 */
97707 runCallback: function(transaction, event) {
97708 var success = !!event.getStatus(),
97709 functionName = success ? 'success' : 'failure',
97710 callback = transaction && transaction.getCallback(),
97711 result;
97712
97713 if (callback) {
97714 // this doesnt make any sense. why do we have both result and data?
97715 // result = Ext.isDefined(event.getResult()) ? event.result : event.data;
97716 result = event.getResult();
97717 if (Ext.isFunction(callback)) {
97718 callback(result, event, success);
97719 } else {
97720 Ext.callback(callback[functionName], callback.scope, [result, event, success]);
97721 Ext.callback(callback.callback, callback.scope, [result, event, success]);
97722 }
97723 }
97724 },
97725
97726 /**
97727 * React to the AJAX request being completed.
97728 * @private
97729 */
97730 onData: function(options, success, response) {
97731 var me = this,
97732 i = 0,
97733 ln, events, event,
97734 transaction, transactions;
97735
97736 if (success) {
97737 events = me.createEvents(response);
97738 for (ln = events.length; i < ln; ++i) {
97739 event = events[i];
97740 transaction = me.getTransaction(event);
97741 me.fireEvent('data', me, event);
97742 if (transaction) {
97743 me.runCallback(transaction, event, true);
97744 Ext.direct.Manager.removeTransaction(transaction);
97745 }
97746 }
97747 } else {
97748 transactions = [].concat(options.transaction);
97749 for (ln = transactions.length; i < ln; ++i) {
97750 transaction = me.getTransaction(transactions[i]);
97751 if (transaction && transaction.getRetryCount() < me.getMaxRetries()) {
97752 transaction.retry();
97753 } else {
97754 event = Ext.create('Ext.direct.ExceptionEvent', {
97755 data: null,
97756 transaction: transaction,
97757 code: Ext.direct.Manager.exceptions.TRANSPORT,
97758 message: 'Unable to connect to the server.',
97759 xhr: response
97760 });
97761
97762 me.fireEvent('data', me, event);
97763 if (transaction) {
97764 me.runCallback(transaction, event, false);
97765 Ext.direct.Manager.removeTransaction(transaction);
97766 }
97767 }
97768 }
97769 }
97770 },
97771
97772 /**
97773 * Get transaction from XHR options.
97774 * @private
97775 * @param {Object} options The options sent to the AJAX request.
97776 * @return {Ext.direct.Transaction/null} The transaction, `null` if not found.
97777 */
97778 getTransaction: function(options) {
97779 return options && options.getTid ? Ext.direct.Manager.getTransaction(options.getTid()) : null;
97780 },
97781
97782 /**
97783 * Configure a direct request.
97784 * @private
97785 * @param {String} action The action being executed.
97786 * @param {Object} method The method being executed.
97787 * @param {Array} args
97788 */
97789 configureRequest: function(action, method, args) {
97790 var me = this,
97791 callData = method.getCallData(args),
97792 data = callData.data,
97793 callback = callData.callback,
97794 scope = callData.scope,
97795 transaction;
97796
97797 transaction = Ext.create('Ext.direct.Transaction', {
97798 provider: me,
97799 args: args,
97800 action: action,
97801 method: method.getName(),
97802 data: data,
97803 callback: scope && Ext.isFunction(callback) ? Ext.Function.bind(callback, scope) : callback
97804 });
97805
97806 if (me.fireEvent('beforecall', me, transaction, method) !== false) {
97807 Ext.direct.Manager.addTransaction(transaction);
97808 me.queueTransaction(transaction);
97809 me.fireEvent('call', me, transaction, method);
97810 }
97811 },
97812
97813 /**
97814 * Gets the AJAX call info for a transaction.
97815 * @private
97816 * @param {Ext.direct.Transaction} transaction The transaction.
97817 * @return {Object} The call params.
97818 */
97819 getCallData: function(transaction) {
97820 return {
97821 action: transaction.getAction(),
97822 method: transaction.getMethod(),
97823 data: transaction.getData(),
97824 type: 'rpc',
97825 tid: transaction.getId()
97826 };
97827 },
97828
97829 /**
97830 * Sends a request to the server.
97831 * @private
97832 * @param {Object/Array} data The data to send.
97833 */
97834 sendRequest : function(data) {
97835 var me = this,
97836 request = {
97837 url: me.getUrl(),
97838 callback: me.onData,
97839 scope: me,
97840 transaction: data,
97841 timeout: me.getTimeout()
97842 }, callData,
97843 enableUrlEncode = me.getEnableUrlEncode(),
97844 i = 0,
97845 ln, params;
97846
97847
97848 if (Ext.isArray(data)) {
97849 callData = [];
97850 for (ln = data.length; i < ln; ++i) {
97851 callData.push(me.getCallData(data[i]));
97852 }
97853 } else {
97854 callData = me.getCallData(data);
97855 }
97856
97857 if (enableUrlEncode) {
97858 params = {};
97859 params[Ext.isString(enableUrlEncode) ? enableUrlEncode : 'data'] = Ext.encode(callData);
97860 request.params = params;
97861 } else {
97862 request.jsonData = callData;
97863 }
97864 Ext.Ajax.request(request);
97865 },
97866
97867 /**
97868 * Add a new transaction to the queue.
97869 * @private
97870 * @param {Ext.direct.Transaction} transaction The transaction.
97871 */
97872 queueTransaction: function(transaction) {
97873 var me = this,
97874 enableBuffer = me.getEnableBuffer();
97875
97876 if (transaction.getForm()) {
97877 me.sendFormRequest(transaction);
97878 return;
97879 }
97880
97881 me.callBuffer.push(transaction);
97882 if (enableBuffer) {
97883 if (!me.callTask) {
97884 me.callTask = Ext.create('Ext.util.DelayedTask', me.combineAndSend, me);
97885 }
97886 me.callTask.delay(Ext.isNumber(enableBuffer) ? enableBuffer : 10);
97887 } else {
97888 me.combineAndSend();
97889 }
97890 },
97891
97892 /**
97893 * Combine any buffered requests and send them off.
97894 * @private
97895 */
97896 combineAndSend : function() {
97897 var buffer = this.callBuffer,
97898 ln = buffer.length;
97899
97900 if (ln > 0) {
97901 this.sendRequest(ln == 1 ? buffer[0] : buffer);
97902 this.callBuffer = [];
97903 }
97904 },
97905
97906 /**
97907 * Configure a form submission request
97908 *
97909 * @param {String} action The action being executed
97910 * @param {Object} method The method being executed
97911 * @param {HTMLElement} form The form being submitted
97912 * @param {Function} [callback] A callback to run after the form submits
97913 * @param {Object} [scope] A scope to execute the callback in
97914 *
97915 * @private
97916 */
97917 configureFormRequest : function(action, method, form, callback, scope) {
97918 var me = this,
97919 transaction, isUpload, params;
97920
97921 transaction = new Ext.direct.Transaction({
97922 provider: me,
97923 action: action,
97924 method: method.getName(),
97925 args: [form, callback, scope],
97926 callback: scope && Ext.isFunction(callback) ? Ext.Function.bind(callback, scope) : callback,
97927 isForm: true
97928 });
97929
97930 if (me.fireEvent('beforecall', me, transaction, method) !== false) {
97931 Ext.direct.Manager.addTransaction(transaction);
97932 isUpload = String(form.getAttribute('enctype')).toLowerCase() == 'multipart/form-data';
97933
97934 params = {
97935 extTID: transaction.id,
97936 extAction: action,
97937 extMethod: method.getName(),
97938 extType: 'rpc',
97939 extUpload: String(isUpload)
97940 };
97941
97942 // change made from typeof callback check to callback.params
97943 // to support addl param passing in DirectSubmit EAC 6/2
97944 Ext.apply(transaction, {
97945 form: Ext.getDom(form),
97946 isUpload: isUpload,
97947 params: callback && Ext.isObject(callback.params) ? Ext.apply(params, callback.params) : params
97948 });
97949
97950 me.fireEvent('call', me, transaction, method);
97951 me.sendFormRequest(transaction);
97952 }
97953 },
97954
97955 /**
97956 * Sends a form request
97957 *
97958 * @param {Ext.direct.Transaction} transaction The transaction to send
97959 *
97960 * @private
97961 */
97962 sendFormRequest : function(transaction) {
97963 var me = this;
97964
97965 Ext.Ajax.request({
97966 url: me.getUrl(),
97967 params: transaction.params,
97968 callback: me.onData,
97969 scope: me,
97970 form: transaction.form,
97971 isUpload: transaction.isUpload,
97972 transaction: transaction
97973 });
97974 }
97975
97976 });
97977
97978 /**
97979 * This class encapsulates a _collection_ of DOM elements, providing methods to filter members, or to perform collective
97980 * actions upon the whole set.
97981 *
97982 * Although they are not listed, this class supports all of the methods of {@link Ext.dom.Element}. The methods from
97983 * these classes will be performed on all the elements in this collection.
97984 *
97985 * All methods return _this_ and can be chained.
97986 *
97987 * Usage:
97988 *
97989 * var els = Ext.select("#some-el div.some-class", true);
97990 * // or select directly from an existing element
97991 * var el = Ext.get('some-el');
97992 * el.select('div.some-class', true);
97993 *
97994 * els.setWidth(100); // all elements become 100 width
97995 * els.hide(true); // all elements fade out and hide
97996 * // or
97997 * els.setWidth(100).hide(true);
97998 */
97999 Ext.define('Ext.dom.CompositeElement', {
98000 alternateClassName: 'Ext.CompositeElement',
98001
98002 extend: Ext.dom.CompositeElementLite ,
98003
98004 // @private
98005 getElement: function(el) {
98006 // In this case just return it, since we already have a reference to it
98007 return el;
98008 },
98009
98010 // @private
98011 transformElement: function(el) {
98012 return Ext.get(el);
98013 }
98014
98015 });
98016
98017 // Using @mixins to include all members of Ext.event.Touch
98018 // into here to keep documentation simpler
98019 /**
98020 * @mixins Ext.event.Touch
98021 *
98022 * Just as {@link Ext.dom.Element} wraps around a native DOM node, {@link Ext.event.Event} wraps the browser's native
98023 * event-object normalizing cross-browser differences such as mechanisms to stop event-propagation along with a method
98024 * to prevent default actions from taking place.
98025 *
98026 * Here is a simple example of how you use it:
98027 *
98028 * @example preview
98029 * Ext.Viewport.add({
98030 * layout: 'fit',
98031 * items: [
98032 * {
98033 * docked: 'top',
98034 * xtype: 'toolbar',
98035 * title: 'Ext.event.Event example!'
98036 * },
98037 * {
98038 * id: 'logger',
98039 * styleHtmlContent: true,
98040 * html: 'Tap somewhere!',
98041 * padding: 5
98042 * }
98043 * ]
98044 * });
98045 *
98046 * Ext.Viewport.element.on({
98047 * tap: function(e, node) {
98048 * var string = '';
98049 *
98050 * string += 'You tapped at: <strong>{ x: ' + e.pageX + ', y: ' + e.pageY + ' }</strong> <i>(e.pageX & e.pageY)</i>';
98051 * string += '<hr />';
98052 * string += 'The HTMLElement you tapped has the className of: <strong>' + e.target.className + '</strong> <i>(e.target)</i>';
98053 * string += '<hr />';
98054 * string += 'The HTMLElement which has the listener has a className of: <strong>' + e.getTarget().className + '</strong> <i>(e.getTarget())</i>';
98055 *
98056 * Ext.getCmp('logger').setHtml(string);
98057 * }
98058 * });
98059 *
98060 * ## Recognizers
98061 *
98062 * Sencha Touch includes a bunch of default event recognizers to know when a user taps, swipes, etc.
98063 *
98064 * For a full list of default recognizers, and more information, please view the {@link Ext.event.recognizer.Recognizer} documentation.
98065 */
98066 Ext.define('Ext.event.Event', {
98067 alternateClassName: 'Ext.EventObject',
98068 isStopped: false,
98069
98070 set: function(name, value) {
98071 if (arguments.length === 1 && typeof name != 'string') {
98072 var info = name;
98073
98074 for (name in info) {
98075 if (info.hasOwnProperty(name)) {
98076 this[name] = info[name];
98077 }
98078 }
98079 }
98080 else {
98081 this[name] = info[name];
98082 }
98083 },
98084
98085 /**
98086 * Stop the event (`preventDefault` and `{@link #stopPropagation}`).
98087 * @chainable
98088 */
98089 stopEvent: function() {
98090 return this.stopPropagation();
98091 },
98092
98093 /**
98094 * Cancels bubbling of the event.
98095 * @chainable
98096 */
98097 stopPropagation: function() {
98098 this.isStopped = true;
98099
98100 return this;
98101 }
98102 });
98103
98104 /**
98105 * @private
98106 * @extends Object
98107 * DOM event. This class really extends {@link Ext.event.Event}, but for documentation
98108 * purposes it's members are listed inside {@link Ext.event.Event}.
98109 */
98110 Ext.define('Ext.event.Dom', {
98111 extend: Ext.event.Event ,
98112
98113 constructor: function(event) {
98114 var target = event.target,
98115 touches;
98116
98117 if (target && target.nodeType !== 1) {
98118 target = target.parentNode;
98119 }
98120 touches = event.changedTouches;
98121 if (touches) {
98122 touches = touches[0];
98123 this.pageX = touches.pageX;
98124 this.pageY = touches.pageY;
98125 }
98126 else {
98127 this.pageX = event.pageX;
98128 this.pageY = event.pageY;
98129 }
98130
98131 this.browserEvent = this.event = event;
98132 this.target = this.delegatedTarget = target;
98133 this.type = event.type;
98134
98135 this.timeStamp = this.time = +event.timeStamp;
98136
98137 return this;
98138 },
98139
98140 /**
98141 * @property {Number} distance
98142 * The distance of the event.
98143 *
98144 * **This is only available when the event type is `swipe` and `pinch`.**
98145 */
98146
98147 /**
98148 * @property {HTMLElement} target
98149 * The target HTMLElement for this event. For example; if you are listening to a tap event and you tap on a `<div>` element,
98150 * this will return that `<div>` element.
98151 */
98152
98153 /**
98154 * @property {Number} pageX The browsers x coordinate of the event.
98155 */
98156
98157 /**
98158 * @property {Number} pageY The browsers y coordinate of the event.
98159 */
98160
98161 stopEvent: function() {
98162 this.preventDefault();
98163
98164 return this.callParent();
98165 },
98166
98167 /**
98168 * Prevents the browsers default handling of the event.
98169 */
98170 preventDefault: function() {
98171 this.browserEvent.preventDefault();
98172 },
98173
98174 /**
98175 * Gets the x coordinate of the event.
98176 * @deprecated 2.0 Please use {@link #pageX} property directly.
98177 * @return {Number}
98178 */
98179 getPageX: function() {
98180 return this.pageX || this.browserEvent.pageX;
98181 },
98182
98183 /**
98184 * Gets the y coordinate of the event.
98185 * @deprecated 2.0 Please use {@link #pageX} property directly.
98186 * @return {Number}
98187 */
98188 getPageY: function() {
98189 return this.pageY || this.browserEvent.pageY;
98190 },
98191
98192 /**
98193 * Gets the X and Y coordinates of the event.
98194 * @deprecated 2.0 Please use the {@link #pageX} and {@link #pageY} properties directly.
98195 * @return {Array}
98196 */
98197 getXY: function() {
98198 if (!this.xy) {
98199 this.xy = [this.getPageX(), this.getPageY()];
98200 }
98201
98202 return this.xy;
98203 },
98204
98205 /**
98206 * Gets the target for the event. Unlike {@link #target}, this returns the main element for your event. So if you are
98207 * listening to a tap event on Ext.Viewport.element, and you tap on an inner element of Ext.Viewport.element, this will
98208 * return Ext.Viewport.element.
98209 *
98210 * If you want the element you tapped on, then use {@link #target}.
98211 *
98212 * @param {String} selector (optional) A simple selector to filter the target or look for an ancestor of the target
98213 * @param {Number/Mixed} [maxDepth=10||document.body] (optional) The max depth to
98214 * search as a number or element (defaults to 10 || document.body)
98215 * @param {Boolean} returnEl (optional) `true` to return a Ext.Element object instead of DOM node.
98216 * @return {HTMLElement}
98217 */
98218 getTarget: function(selector, maxDepth, returnEl) {
98219 if (arguments.length === 0) {
98220 return this.delegatedTarget;
98221 }
98222
98223 return selector ? Ext.fly(this.target).findParent(selector, maxDepth, returnEl) : (returnEl ? Ext.get(this.target) : this.target);
98224 },
98225
98226 /**
98227 * Returns the time of the event.
98228 * @return {Date}
98229 */
98230 getTime: function() {
98231 return this.time;
98232 },
98233
98234 setDelegatedTarget: function(target) {
98235 this.delegatedTarget = target;
98236 },
98237
98238 makeUnpreventable: function() {
98239 this.browserEvent.preventDefault = Ext.emptyFn;
98240 }
98241 });
98242
98243 /**
98244 * @private
98245 * Touch event.
98246 */
98247 Ext.define('Ext.event.Touch', {
98248 extend: Ext.event.Dom ,
98249
98250
98251
98252
98253
98254 constructor: function(event, info, map, list) {
98255 var touches = [],
98256 touch, i, ln, identifier;
98257
98258 if (info) {
98259 this.set(info);
98260 }
98261
98262 this.changedTouches = this.cloneTouches(event.changedTouches, map);
98263
98264 for (i = 0, ln = list.length; i < ln; i++) {
98265 identifier = list[i];
98266 touches.push(map[identifier]);
98267 }
98268
98269 this.touches = touches;
98270 this.targetTouches = touches.slice();
98271
98272 touch = this.changedTouches[0];
98273
98274 this.callSuper([event]);
98275
98276 this.target = this.delegatedTarget = touch.target;
98277 this.pageX = touch.pageX;
98278 this.pageY = touch.pageY;
98279 },
98280
98281 cloneTouches: function(touches, map) {
98282 var clonedTouches = [],
98283 i, ln, touch, identifier;
98284
98285 for (i = 0,ln = touches.length; i < ln; i++) {
98286 touch = touches[i];
98287 identifier = touch.identifier;
98288 clonedTouches[i] = map[identifier];
98289 }
98290
98291 return clonedTouches;
98292 }
98293 });
98294
98295 /**
98296 * @private
98297 */
98298 Ext.define('Ext.event.publisher.ComponentDelegation', {
98299 extend: Ext.event.publisher.Publisher ,
98300
98301
98302
98303
98304
98305
98306 targetType: 'component',
98307
98308 optimizedSelectorRegex: /^#([\w\-]+)((?:[\s]*)>(?:[\s]*)|(?:\s*))([\w\-]+)$/i,
98309
98310 handledEvents: ['*'],
98311
98312 getSubscribers: function(eventName, createIfNotExist) {
98313 var subscribers = this.subscribers,
98314 eventSubscribers = subscribers[eventName];
98315
98316 if (!eventSubscribers && createIfNotExist) {
98317 eventSubscribers = subscribers[eventName] = {
98318 type: {
98319 $length: 0
98320 },
98321 selector: [],
98322 $length: 0
98323 }
98324 }
98325
98326 return eventSubscribers;
98327 },
98328
98329 subscribe: function(target, eventName) {
98330 // Ignore id-only selectors since they are already handled
98331 if (this.idSelectorRegex.test(target)) {
98332 return false;
98333 }
98334
98335 var optimizedSelector = target.match(this.optimizedSelectorRegex),
98336 subscribers = this.getSubscribers(eventName, true),
98337 typeSubscribers = subscribers.type,
98338 selectorSubscribers = subscribers.selector,
98339 id, isDescendant, type, map, subMap;
98340
98341 if (optimizedSelector !== null) {
98342 id = optimizedSelector[1];
98343 isDescendant = optimizedSelector[2].indexOf('>') === -1;
98344 type = optimizedSelector[3];
98345
98346 map = typeSubscribers[type];
98347
98348 if (!map) {
98349 typeSubscribers[type] = map = {
98350 descendents: {
98351 $length: 0
98352 },
98353 children: {
98354 $length: 0
98355 },
98356 $length: 0
98357 }
98358 }
98359
98360 subMap = isDescendant ? map.descendents : map.children;
98361
98362 if (subMap.hasOwnProperty(id)) {
98363 subMap[id]++;
98364 return true;
98365 }
98366
98367 subMap[id] = 1;
98368 subMap.$length++;
98369 map.$length++;
98370 typeSubscribers.$length++;
98371 }
98372 else {
98373 if (selectorSubscribers.hasOwnProperty(target)) {
98374 selectorSubscribers[target]++;
98375 return true;
98376 }
98377
98378 selectorSubscribers[target] = 1;
98379 selectorSubscribers.push(target);
98380 }
98381
98382 subscribers.$length++;
98383
98384 return true;
98385 },
98386
98387 unsubscribe: function(target, eventName, all) {
98388 var subscribers = this.getSubscribers(eventName);
98389
98390 if (!subscribers) {
98391 return false;
98392 }
98393
98394 var match = target.match(this.optimizedSelectorRegex),
98395 typeSubscribers = subscribers.type,
98396 selectorSubscribers = subscribers.selector,
98397 id, isDescendant, type, map, subMap;
98398
98399 all = Boolean(all);
98400
98401 if (match !== null) {
98402 id = match[1];
98403 isDescendant = match[2].indexOf('>') === -1;
98404 type = match[3];
98405
98406 map = typeSubscribers[type];
98407
98408 if (!map) {
98409 return true;
98410 }
98411
98412 subMap = isDescendant ? map.descendents : map.children;
98413
98414 if (!subMap.hasOwnProperty(id) || (!all && --subMap[id] > 0)) {
98415 return true;
98416 }
98417
98418 delete subMap[id];
98419 subMap.$length--;
98420 map.$length--;
98421 typeSubscribers.$length--;
98422 }
98423 else {
98424 if (!selectorSubscribers.hasOwnProperty(target) || (!all && --selectorSubscribers[target] > 0)) {
98425 return true;
98426 }
98427
98428 delete selectorSubscribers[target];
98429 Ext.Array.remove(selectorSubscribers, target);
98430 }
98431
98432 if (--subscribers.$length === 0) {
98433 delete this.subscribers[eventName];
98434 }
98435
98436 return true;
98437 },
98438
98439 notify: function(target, eventName) {
98440 var subscribers = this.getSubscribers(eventName),
98441 id, component;
98442
98443 if (!subscribers || subscribers.$length === 0) {
98444 return false;
98445 }
98446
98447 id = target.substr(1);
98448 component = Ext.ComponentManager.get(id);
98449
98450 if (component) {
98451 this.dispatcher.doAddListener(this.targetType, target, eventName, 'publish', this, {
98452 args: [eventName, component]
98453 }, 'before');
98454 }
98455 },
98456
98457 matchesSelector: function(component, selector) {
98458 return Ext.ComponentQuery.is(component, selector);
98459 },
98460
98461 dispatch: function(target, eventName, args, connectedController) {
98462 this.dispatcher.doDispatchEvent(this.targetType, target, eventName, args, null, connectedController);
98463 },
98464
98465 publish: function(eventName, component) {
98466 var subscribers = this.getSubscribers(eventName);
98467
98468 if (!subscribers) {
98469 return;
98470 }
98471
98472 var eventController = arguments[arguments.length - 1],
98473 typeSubscribers = subscribers.type,
98474 selectorSubscribers = subscribers.selector,
98475 args = Array.prototype.slice.call(arguments, 2, -2),
98476 types = component.xtypesChain,
98477 descendentsSubscribers, childrenSubscribers,
98478 parentId, ancestorIds, ancestorId, parentComponent,
98479 selector,
98480 i, ln, type, j, subLn;
98481
98482 for (i = 0, ln = types.length; i < ln; i++) {
98483 type = types[i];
98484
98485 subscribers = typeSubscribers[type];
98486
98487 if (subscribers && subscribers.$length > 0) {
98488 descendentsSubscribers = subscribers.descendents;
98489
98490 if (descendentsSubscribers.$length > 0) {
98491 if (!ancestorIds) {
98492 ancestorIds = component.getAncestorIds();
98493 }
98494
98495 for (j = 0, subLn = ancestorIds.length; j < subLn; j++) {
98496 ancestorId = ancestorIds[j];
98497
98498 if (descendentsSubscribers.hasOwnProperty(ancestorId)) {
98499 this.dispatch('#' + ancestorId + ' ' + type, eventName, args, eventController);
98500 }
98501
98502 }
98503 }
98504
98505 childrenSubscribers = subscribers.children;
98506
98507 if (childrenSubscribers.$length > 0) {
98508 if (!parentId) {
98509 if (ancestorIds) {
98510 parentId = ancestorIds[0];
98511 }
98512 else {
98513 parentComponent = component.getParent();
98514 if (parentComponent) {
98515 parentId = parentComponent.getId();
98516 }
98517 }
98518 }
98519
98520 if (parentId) {
98521 if (childrenSubscribers.hasOwnProperty(parentId)) {
98522 this.dispatch('#' + parentId + ' > ' + type, eventName, args, eventController);
98523 }
98524 }
98525 }
98526 }
98527 }
98528
98529 ln = selectorSubscribers.length;
98530
98531 if (ln > 0) {
98532 for (i = 0; i < ln; i++) {
98533 selector = selectorSubscribers[i];
98534
98535 if (this.matchesSelector(component, selector)) {
98536 this.dispatch(selector, eventName, args, eventController);
98537 }
98538 }
98539 }
98540 }
98541 });
98542
98543 /**
98544 * @private
98545 */
98546 Ext.define('Ext.event.publisher.ComponentPaint', {
98547
98548 extend: Ext.event.publisher.Publisher ,
98549
98550 targetType: 'component',
98551
98552 handledEvents: ['erased'],
98553
98554 eventNames: {
98555 painted: 'painted',
98556 erased: 'erased'
98557 },
98558
98559 constructor: function() {
98560 this.callParent(arguments);
98561
98562 this.hiddenQueue = {};
98563 this.renderedQueue = {};
98564 },
98565
98566 getSubscribers: function(eventName, createIfNotExist) {
98567 var subscribers = this.subscribers;
98568
98569 if (!subscribers.hasOwnProperty(eventName)) {
98570 if (!createIfNotExist) {
98571 return null;
98572 }
98573
98574 subscribers[eventName] = {
98575 $length: 0
98576 };
98577 }
98578
98579 return subscribers[eventName];
98580 },
98581
98582 setDispatcher: function(dispatcher) {
98583 var targetType = this.targetType;
98584
98585 dispatcher.doAddListener(targetType, '*', 'renderedchange', 'onBeforeComponentRenderedChange', this, null, 'before');
98586 dispatcher.doAddListener(targetType, '*', 'hiddenchange', 'onBeforeComponentHiddenChange', this, null, 'before');
98587 dispatcher.doAddListener(targetType, '*', 'renderedchange', 'onComponentRenderedChange', this, null, 'after');
98588 dispatcher.doAddListener(targetType, '*', 'hiddenchange', 'onComponentHiddenChange', this, null, 'after');
98589
98590 return this.callParent(arguments);
98591 },
98592
98593 subscribe: function(target, eventName) {
98594 var match = target.match(this.idSelectorRegex),
98595 subscribers,
98596 id;
98597
98598 if (!match) {
98599 return false;
98600 }
98601
98602 id = match[1];
98603
98604 subscribers = this.getSubscribers(eventName, true);
98605
98606 if (subscribers.hasOwnProperty(id)) {
98607 subscribers[id]++;
98608 return true;
98609 }
98610
98611 subscribers[id] = 1;
98612 subscribers.$length++;
98613
98614 return true;
98615 },
98616
98617 unsubscribe: function(target, eventName, all) {
98618 var match = target.match(this.idSelectorRegex),
98619 subscribers,
98620 id;
98621
98622 if (!match || !(subscribers = this.getSubscribers(eventName))) {
98623 return false;
98624 }
98625
98626 id = match[1];
98627
98628 if (!subscribers.hasOwnProperty(id) || (!all && --subscribers[id] > 0)) {
98629 return true;
98630 }
98631
98632 delete subscribers[id];
98633
98634 if (--subscribers.$length === 0) {
98635 delete this.subscribers[eventName];
98636 }
98637
98638 return true;
98639 },
98640
98641 onBeforeComponentRenderedChange: function(container, component, rendered) {
98642 var eventNames = this.eventNames,
98643 eventName = rendered ? eventNames.painted : eventNames.erased,
98644 subscribers = this.getSubscribers(eventName),
98645 queue;
98646
98647 if (subscribers && subscribers.$length > 0) {
98648 this.renderedQueue[component.getId()] = queue = [];
98649 this.publish(subscribers, component, eventName, queue);
98650 }
98651 },
98652
98653 onBeforeComponentHiddenChange: function(component, hidden) {
98654 var eventNames = this.eventNames,
98655 eventName = hidden ? eventNames.erased : eventNames.painted,
98656 subscribers = this.getSubscribers(eventName),
98657 queue;
98658
98659 if (subscribers && subscribers.$length > 0) {
98660 this.hiddenQueue[component.getId()] = queue = [];
98661 this.publish(subscribers, component, eventName, queue);
98662 }
98663 },
98664
98665 onComponentRenderedChange: function(container, component) {
98666 var renderedQueue = this.renderedQueue,
98667 id = component.getId(),
98668 queue;
98669
98670 if (!renderedQueue.hasOwnProperty(id)) {
98671 return;
98672 }
98673
98674 queue = renderedQueue[id];
98675 delete renderedQueue[id];
98676
98677 if (queue.length > 0) {
98678 this.dispatchQueue(queue);
98679 }
98680 },
98681
98682 onComponentHiddenChange: function(component) {
98683 var hiddenQueue = this.hiddenQueue,
98684 id = component.getId(),
98685 queue;
98686
98687 if (!hiddenQueue.hasOwnProperty(id)) {
98688 return;
98689 }
98690
98691 queue = hiddenQueue[id];
98692 delete hiddenQueue[id];
98693
98694 if (queue.length > 0) {
98695 this.dispatchQueue(queue);
98696 }
98697 },
98698
98699 dispatchQueue: function(dispatchingQueue) {
98700 var dispatcher = this.dispatcher,
98701 targetType = this.targetType,
98702 eventNames = this.eventNames,
98703 queue = dispatchingQueue.slice(),
98704 ln = queue.length,
98705 i, item, component, eventName, isPainted;
98706
98707 dispatchingQueue.length = 0;
98708
98709 if (ln > 0) {
98710 for (i = 0; i < ln; i++) {
98711 item = queue[i];
98712 component = item.component;
98713 eventName = item.eventName;
98714 isPainted = component.isPainted();
98715
98716 if ((eventName === eventNames.painted && isPainted) || eventName === eventNames.erased && !isPainted) {
98717 dispatcher.doDispatchEvent(targetType, '#' + item.id, eventName, [component]);
98718 }
98719 }
98720 queue.length = 0;
98721 }
98722
98723 },
98724
98725 publish: function(subscribers, component, eventName, dispatchingQueue) {
98726 var id = component.getId(),
98727 needsDispatching = false,
98728 eventNames, items, i, ln, isPainted;
98729
98730 if (subscribers[id]) {
98731 eventNames = this.eventNames;
98732
98733 isPainted = component.isPainted();
98734
98735 if ((eventName === eventNames.painted && !isPainted) || eventName === eventNames.erased && isPainted) {
98736 needsDispatching = true;
98737 }
98738 else {
98739 return this;
98740 }
98741 }
98742
98743 if (component.isContainer) {
98744 items = component.getItems().items;
98745
98746 for (i = 0,ln = items.length; i < ln; i++) {
98747 this.publish(subscribers, items[i], eventName, dispatchingQueue);
98748 }
98749 }
98750 else if (component.isDecorator) {
98751 this.publish(subscribers, component.getComponent(), eventName, dispatchingQueue);
98752 }
98753
98754 if (needsDispatching) {
98755 dispatchingQueue.push({
98756 id: id,
98757 eventName: eventName,
98758 component: component
98759 });
98760 }
98761 }
98762 });
98763
98764 /**
98765 * @private
98766 */
98767 Ext.define('Ext.event.publisher.ComponentSize', {
98768
98769 extend: Ext.event.publisher.Publisher ,
98770
98771
98772
98773
98774
98775 targetType: 'component',
98776
98777 handledEvents: ['resize', 'innerresize'],
98778
98779 constructor: function() {
98780 this.callParent(arguments);
98781
98782 this.sizeMonitors = {};
98783 },
98784
98785 getSubscribers: function(target, createIfNotExist) {
98786 var subscribers = this.subscribers;
98787
98788 if (!subscribers.hasOwnProperty(target)) {
98789 if (!createIfNotExist) {
98790 return null;
98791 }
98792
98793 subscribers[target] = {
98794 $length: 0
98795 };
98796 }
98797
98798 return subscribers[target];
98799 },
98800
98801 subscribe: function(target, eventName) {
98802 var match = target.match(this.idSelectorRegex),
98803 sizeMonitors = this.sizeMonitors,
98804 dispatcher = this.dispatcher,
98805 targetType = this.targetType,
98806 subscribers, component, id;
98807
98808 if (!match) {
98809 return false;
98810 }
98811
98812 id = match[1];
98813 subscribers = this.getSubscribers(target, true);
98814 subscribers.$length++;
98815
98816 if (subscribers.hasOwnProperty(eventName)) {
98817 subscribers[eventName]++;
98818 return true;
98819 }
98820
98821 if (subscribers.$length === 1) {
98822 dispatcher.addListener(targetType, target, 'painted', 'onComponentPainted', this, null, 'before');
98823 }
98824
98825 component = Ext.ComponentManager.get(id);
98826
98827 //<debug error>
98828 if (!component) {
98829 Ext.Logger.error("Adding a listener to the 'resize' event of a non-existing component");
98830 }
98831 //</debug>
98832
98833 if (!sizeMonitors[target]) {
98834 sizeMonitors[target] = {};
98835 }
98836
98837 sizeMonitors[target][eventName] = new Ext.util.SizeMonitor({
98838 element: eventName === 'resize' ? component.element : component.innerElement,
98839 callback: this.onComponentSizeChange,
98840 scope: this,
98841 args: [this, target, eventName]
98842 });
98843
98844 subscribers[eventName] = 1;
98845 return true;
98846 },
98847
98848 unsubscribe: function(target, eventName, all) {
98849 var match = target.match(this.idSelectorRegex),
98850 dispatcher = this.dispatcher,
98851 targetType = this.targetType,
98852 sizeMonitors = this.sizeMonitors,
98853 subscribers,
98854 id;
98855
98856 if (!match || !(subscribers = this.getSubscribers(target))) {
98857 return false;
98858 }
98859
98860 id = match[1];
98861
98862 if (!subscribers.hasOwnProperty(eventName) || (!all && --subscribers[eventName] > 0)) {
98863 return true;
98864 }
98865
98866 delete subscribers[eventName];
98867
98868 sizeMonitors[target][eventName].destroy();
98869 delete sizeMonitors[target][eventName];
98870
98871 if (--subscribers.$length === 0) {
98872 delete sizeMonitors[target];
98873 delete this.subscribers[target];
98874 dispatcher.removeListener(targetType, target, 'painted', 'onComponentPainted', this, 'before');
98875 }
98876
98877 return true;
98878 },
98879
98880 onComponentPainted: function(component) {
98881 var target = component.getObservableId(),
98882 sizeMonitors = this.sizeMonitors[target];
98883
98884 if (sizeMonitors.resize) {
98885 sizeMonitors.resize.refresh();
98886 }
98887
98888 if (sizeMonitors.innerresize) {
98889 sizeMonitors.innerresize.refresh();
98890 }
98891 },
98892
98893 onComponentSizeChange: function(component, observableId, eventName) {
98894 this.dispatcher.doDispatchEvent(this.targetType, observableId, eventName, [component]);
98895 }
98896 });
98897
98898 /**
98899 * @private
98900 */
98901 Ext.define('Ext.event.publisher.Dom', {
98902 extend: Ext.event.publisher.Publisher ,
98903
98904
98905
98906
98907
98908
98909
98910 targetType: 'element',
98911
98912 idOrClassSelectorRegex: /^([#|\.])([\w\-]+)$/,
98913
98914 handledEvents: ['focus', 'blur', 'paste', 'input', 'change',
98915 'keyup', 'keydown', 'keypress', 'submit',
98916 'transitionend', 'animationstart', 'animationend'],
98917
98918 classNameSplitRegex: /\s+/,
98919
98920 SELECTOR_ALL: '*',
98921
98922 constructor: function() {
98923 var eventNames = this.getHandledEvents(),
98924 eventNameMap = {},
98925 i, ln, eventName, vendorEventName;
98926
98927 this.doBubbleEventsMap = {
98928 'click': true,
98929 'submit': true,
98930 'mousedown': true,
98931 'mousemove': true,
98932 'mouseup': true,
98933 'mouseover': true,
98934 'mouseout': true,
98935 'transitionend': true
98936 };
98937
98938 this.onEvent = Ext.Function.bind(this.onEvent, this);
98939
98940 for (i = 0,ln = eventNames.length; i < ln; i++) {
98941 eventName = eventNames[i];
98942 vendorEventName = this.getVendorEventName(eventName);
98943 eventNameMap[vendorEventName] = eventName;
98944
98945 this.attachListener(vendorEventName);
98946 }
98947
98948 this.eventNameMap = eventNameMap;
98949
98950 return this.callParent();
98951 },
98952
98953 getSubscribers: function(eventName) {
98954 var subscribers = this.subscribers,
98955 eventSubscribers = subscribers[eventName];
98956
98957 if (!eventSubscribers) {
98958 eventSubscribers = subscribers[eventName] = {
98959 id: {
98960 $length: 0
98961 },
98962 className: {
98963 $length: 0
98964 },
98965 selector: [],
98966 all: 0,
98967 $length: 0
98968 }
98969 }
98970
98971 return eventSubscribers;
98972 },
98973
98974 getVendorEventName: function(eventName) {
98975 if (Ext.browser.is.WebKit) {
98976 if (eventName === 'transitionend') {
98977 eventName = Ext.browser.getVendorProperyName('transitionEnd');
98978 }
98979 else if (eventName === 'animationstart') {
98980 eventName = Ext.browser.getVendorProperyName('animationStart');
98981 }
98982 else if (eventName === 'animationend') {
98983 eventName = Ext.browser.getVendorProperyName('animationEnd');
98984 }
98985 }
98986
98987 return eventName;
98988 },
98989
98990 bindListeners: function (doc, bind) {
98991 var handlesEvents = this.getHandledEvents(),
98992 handlesEventsLength = handlesEvents.length,
98993 i;
98994
98995 for (i = 0; i < handlesEventsLength; i++) {
98996 this.bindListener(doc, this.getVendorEventName(handlesEvents[i]), bind);
98997 }
98998 },
98999
99000 bindListener: function (doc, eventName, bind) {
99001 if (bind) {
99002 this.attachListener(eventName, doc);
99003 } else {
99004 this.removeListener(eventName, doc);
99005 }
99006 return this
99007 },
99008
99009 attachListener: function(eventName, doc) {
99010 if (!doc) {
99011 doc = document;
99012 }
99013
99014 var defaultView = doc.defaultView;
99015
99016 // Some AndroidStock browsers (HP Slate for example) will not process any touch events unless a listener is added to document or body
99017 // this listener must be to a touch event (touchstart, touchmove, touchend)
99018 if ((Ext.os.is.iOS && Ext.os.version.getMajor() < 5) || Ext.browser.is.AndroidStock) {
99019 document.addEventListener(eventName, this.onEvent, !this.doesEventBubble(eventName));
99020 }
99021 else if (defaultView && defaultView.addEventListener) {
99022 doc.defaultView.addEventListener(eventName, this.onEvent, !this.doesEventBubble(eventName));
99023 }
99024 else {
99025 doc.addEventListener(eventName, this.onEvent, !this.doesEventBubble(eventName));
99026 }
99027 return this;
99028 },
99029
99030 removeListener: function(eventName, doc) {
99031 if (!doc) {
99032 doc = document;
99033 }
99034
99035 var defaultView = doc.defaultView;
99036
99037 if ((Ext.os.is.iOS && Ext.os.version.getMajor() < 5) && Ext.browser.is.AndroidStock) {
99038 document.removeEventListener(eventName, this.onEvent, !this.doesEventBubble(eventName));
99039 }
99040 else if (defaultView && defaultView.addEventListener) {
99041 doc.defaultView.removeEventListener(eventName, this.onEvent, !this.doesEventBubble(eventName));
99042 }
99043 else {
99044 doc.removeEventListener(eventName, this.onEvent, !this.doesEventBubble(eventName));
99045 }
99046 return this;
99047 },
99048
99049 doesEventBubble: function(eventName) {
99050 return !!this.doBubbleEventsMap[eventName];
99051 },
99052
99053 subscribe: function(target, eventName) {
99054 if (!this.handles(eventName)) {
99055 return false;
99056 }
99057
99058 var idOrClassSelectorMatch = target.match(this.idOrClassSelectorRegex),
99059 subscribers = this.getSubscribers(eventName),
99060 idSubscribers = subscribers.id,
99061 classNameSubscribers = subscribers.className,
99062 selectorSubscribers = subscribers.selector,
99063 type, value;
99064
99065 if (idOrClassSelectorMatch !== null) {
99066 type = idOrClassSelectorMatch[1];
99067 value = idOrClassSelectorMatch[2];
99068
99069 if (type === '#') {
99070 if (idSubscribers.hasOwnProperty(value)) {
99071 idSubscribers[value]++;
99072 return true;
99073 }
99074
99075 idSubscribers[value] = 1;
99076 idSubscribers.$length++;
99077 }
99078 else {
99079 if (classNameSubscribers.hasOwnProperty(value)) {
99080 classNameSubscribers[value]++;
99081 return true;
99082 }
99083
99084 classNameSubscribers[value] = 1;
99085 classNameSubscribers.$length++;
99086 }
99087 }
99088 else {
99089 if (target === this.SELECTOR_ALL) {
99090 subscribers.all++;
99091 }
99092 else {
99093 if (selectorSubscribers.hasOwnProperty(target)) {
99094 selectorSubscribers[target]++;
99095 return true;
99096 }
99097
99098 selectorSubscribers[target] = 1;
99099 selectorSubscribers.push(target);
99100 }
99101 }
99102
99103 subscribers.$length++;
99104
99105 return true;
99106 },
99107
99108 unsubscribe: function(target, eventName, all) {
99109 if (!this.handles(eventName)) {
99110 return false;
99111 }
99112
99113 var idOrClassSelectorMatch = target.match(this.idOrClassSelectorRegex),
99114 subscribers = this.getSubscribers(eventName),
99115 idSubscribers = subscribers.id,
99116 classNameSubscribers = subscribers.className,
99117 selectorSubscribers = subscribers.selector,
99118 type, value;
99119
99120 all = Boolean(all);
99121
99122 if (idOrClassSelectorMatch !== null) {
99123 type = idOrClassSelectorMatch[1];
99124 value = idOrClassSelectorMatch[2];
99125
99126 if (type === '#') {
99127 if (!idSubscribers.hasOwnProperty(value) || (!all && --idSubscribers[value] > 0)) {
99128 return true;
99129 }
99130
99131 delete idSubscribers[value];
99132 idSubscribers.$length--;
99133 }
99134 else {
99135 if (!classNameSubscribers.hasOwnProperty(value) || (!all && --classNameSubscribers[value] > 0)) {
99136 return true;
99137 }
99138
99139 delete classNameSubscribers[value];
99140 classNameSubscribers.$length--;
99141 }
99142 }
99143 else {
99144 if (target === this.SELECTOR_ALL) {
99145 if (all) {
99146 subscribers.all = 0;
99147 }
99148 else {
99149 subscribers.all--;
99150 }
99151 }
99152 else {
99153 if (!selectorSubscribers.hasOwnProperty(target) || (!all && --selectorSubscribers[target] > 0)) {
99154 return true;
99155 }
99156
99157 delete selectorSubscribers[target];
99158 Ext.Array.remove(selectorSubscribers, target);
99159 }
99160 }
99161
99162 subscribers.$length--;
99163
99164 return true;
99165 },
99166
99167 getElementTarget: function(target) {
99168 if (target.nodeType !== 1) {
99169 target = target.parentNode;
99170
99171 if (!target || target.nodeType !== 1) {
99172 return null;
99173 }
99174 }
99175
99176 return target;
99177 },
99178
99179 getBubblingTargets: function(target) {
99180 var targets = [];
99181
99182 if (!target) {
99183 return targets;
99184 }
99185
99186 do {
99187 targets[targets.length] = target;
99188
99189 target = target.parentNode;
99190 } while (target && target.nodeType === 1);
99191
99192 return targets;
99193 },
99194
99195 dispatch: function(target, eventName, args) {
99196 args.push(args[0].target);
99197 this.callParent(arguments);
99198 },
99199
99200 publish: function(eventName, targets, event) {
99201 var subscribers = this.getSubscribers(eventName),
99202 wildcardSubscribers;
99203
99204 if (subscribers.$length === 0 || !this.doPublish(subscribers, eventName, targets, event)) {
99205 wildcardSubscribers = this.getSubscribers('*');
99206
99207 if (wildcardSubscribers.$length > 0) {
99208 this.doPublish(wildcardSubscribers, eventName, targets, event);
99209 }
99210 }
99211
99212 return this;
99213 },
99214
99215 doPublish: function(subscribers, eventName, targets, event) {
99216 var idSubscribers = subscribers.id,
99217 classNameSubscribers = subscribers.className,
99218 selectorSubscribers = subscribers.selector,
99219 hasIdSubscribers = idSubscribers.$length > 0,
99220 hasClassNameSubscribers = classNameSubscribers.$length > 0,
99221 hasSelectorSubscribers = selectorSubscribers.length > 0,
99222 hasAllSubscribers = subscribers.all > 0,
99223 isClassNameHandled = {},
99224 args = [event],
99225 hasDispatched = false,
99226 classNameSplitRegex = this.classNameSplitRegex,
99227 i, ln, j, subLn, target, id, className, classNames, selector;
99228
99229 for (i = 0,ln = targets.length; i < ln; i++) {
99230 target = targets[i];
99231 event.setDelegatedTarget(target);
99232
99233 if (hasIdSubscribers) {
99234 // We use getAttribute instead of referencing id here as forms can have there properties overridden by children
99235 // Example:
99236 // <form id="myForm">
99237 // <input name="id">
99238 // </form>
99239 // form.id === input node named id whereas form.getAttribute("id") === "myForm"
99240 id = target.getAttribute("id");
99241
99242 if (id) {
99243 if (idSubscribers.hasOwnProperty(id)) {
99244 hasDispatched = true;
99245 this.dispatch('#' + id, eventName, args);
99246 }
99247 }
99248 }
99249
99250 if (hasClassNameSubscribers) {
99251 className = target.className;
99252
99253 if (className) {
99254 classNames = className.split(classNameSplitRegex);
99255
99256 for (j = 0,subLn = classNames.length; j < subLn; j++) {
99257 className = classNames[j];
99258
99259 if (!isClassNameHandled[className]) {
99260 isClassNameHandled[className] = true;
99261
99262 if (classNameSubscribers.hasOwnProperty(className)) {
99263 hasDispatched = true;
99264 this.dispatch('.' + className, eventName, args);
99265 }
99266 }
99267 }
99268 }
99269 }
99270
99271 // Stop propagation
99272 if (event.isStopped) {
99273 return hasDispatched;
99274 }
99275 }
99276
99277 if (hasAllSubscribers && !hasDispatched) {
99278 event.setDelegatedTarget(event.browserEvent.target);
99279 hasDispatched = true;
99280 this.dispatch(this.SELECTOR_ALL, eventName, args);
99281 if (event.isStopped) {
99282 return hasDispatched;
99283 }
99284 }
99285
99286 if (hasSelectorSubscribers) {
99287 for (j = 0,subLn = targets.length; j < subLn; j++) {
99288 target = targets[j];
99289
99290 for (i = 0,ln = selectorSubscribers.length; i < ln; i++) {
99291 selector = selectorSubscribers[i];
99292
99293 if (this.matchesSelector(target, selector)) {
99294 event.setDelegatedTarget(target);
99295 hasDispatched = true;
99296 this.dispatch(selector, eventName, args);
99297 }
99298
99299 if (event.isStopped) {
99300 return hasDispatched;
99301 }
99302 }
99303 }
99304 }
99305
99306 return hasDispatched;
99307 },
99308
99309 matchesSelector: function() {
99310 var test = Element.prototype,
99311 matchesSelector =
99312 ('webkitMatchesSelector' in test) ? 'webkitMatchesSelector' :
99313 (('msMatchesSelector' in test) ? 'msMatchesSelector' : ('mozMatchesSelector' in test ? 'mozMatchesSelector' : null));
99314
99315 if (matchesSelector) {
99316 return function(element, selector) {
99317 return element[matchesSelector](selector);
99318 }
99319 }
99320
99321 return function(element, selector) {
99322 Ext.DomQuery.is(element, selector);
99323 }
99324 }(),
99325
99326 onEvent: function(e) {
99327 var eventName = this.eventNameMap[e.type];
99328 // Set the current frame start time to be the timestamp of the event.
99329 Ext.frameStartTime = e.timeStamp;
99330
99331 if (!eventName || this.getSubscribersCount(eventName) === 0) {
99332 return;
99333 }
99334
99335 var target = this.getElementTarget(e.target),
99336 targets;
99337
99338 if (!target) {
99339 return;
99340 }
99341
99342 if (this.doesEventBubble(eventName)) {
99343 targets = this.getBubblingTargets(target);
99344 }
99345 else {
99346 targets = [target];
99347 }
99348
99349 this.publish(eventName, targets, new Ext.event.Dom(e));
99350 },
99351
99352 //<debug>
99353 hasSubscriber: function(target, eventName) {
99354 if (!this.handles(eventName)) {
99355 return false;
99356 }
99357
99358 var match = target.match(this.idOrClassSelectorRegex),
99359 subscribers = this.getSubscribers(eventName),
99360 type, value;
99361
99362 if (match !== null) {
99363 type = match[1];
99364 value = match[2];
99365
99366 if (type === '#') {
99367 return subscribers.id.hasOwnProperty(value);
99368 }
99369 else {
99370 return subscribers.className.hasOwnProperty(value);
99371 }
99372 }
99373 else {
99374 return (subscribers.selector.hasOwnProperty(target) && Ext.Array.indexOf(subscribers.selector, target) !== -1);
99375 }
99376
99377 return false;
99378 },
99379 //</debug>
99380
99381 getSubscribersCount: function(eventName) {
99382 if (!this.handles(eventName)) {
99383 return 0;
99384 }
99385
99386 return this.getSubscribers(eventName).$length + this.getSubscribers('*').$length;
99387 }
99388
99389 });
99390
99391 /**
99392 * @private
99393 */
99394 Ext.define('Ext.util.paintmonitor.Abstract', {
99395
99396 config: {
99397 element: null,
99398
99399 callback: Ext.emptyFn,
99400
99401 scope: null,
99402
99403 args: []
99404 },
99405
99406 eventName: '',
99407
99408 monitorClass: '',
99409
99410 constructor: function(config) {
99411 this.onElementPainted = Ext.Function.bind(this.onElementPainted, this);
99412
99413 this.initConfig(config);
99414 },
99415
99416 bindListeners: function(bind) {
99417 this.monitorElement[bind ? 'addEventListener' : 'removeEventListener'](this.eventName, this.onElementPainted, true);
99418 },
99419
99420 applyElement: function(element) {
99421 if (element) {
99422 return Ext.get(element);
99423 }
99424 },
99425
99426 updateElement: function(element) {
99427 this.monitorElement = Ext.Element.create({
99428 classList: ['x-paint-monitor', this.monitorClass]
99429 }, true);
99430
99431 element.appendChild(this.monitorElement);
99432 element.addCls('x-paint-monitored');
99433 this.bindListeners(true);
99434 },
99435
99436 onElementPainted: function() {},
99437
99438 destroy: function() {
99439 var monitorElement = this.monitorElement,
99440 parentNode = monitorElement.parentNode,
99441 element = this.getElement();
99442
99443 this.bindListeners(false);
99444 delete this.monitorElement;
99445
99446 if (element && !element.isDestroyed) {
99447 element.removeCls('x-paint-monitored');
99448 delete this._element;
99449 }
99450
99451 if (parentNode) {
99452 parentNode.removeChild(monitorElement);
99453 }
99454
99455 this.callSuper();
99456 }
99457 });
99458
99459 /**
99460 * @private
99461 */
99462 Ext.define('Ext.util.paintmonitor.CssAnimation', {
99463
99464 extend: Ext.util.paintmonitor.Abstract ,
99465
99466 eventName: Ext.browser.is.WebKit ? 'webkitAnimationEnd' : 'animationend',
99467
99468 monitorClass: 'cssanimation',
99469
99470 onElementPainted: function(e) {
99471 if (e.animationName === 'x-paint-monitor-helper') {
99472 this.getCallback().apply(this.getScope(), this.getArgs());
99473 }
99474 }
99475 });
99476
99477 /**
99478 *
99479 */
99480 Ext.define('Ext.util.PaintMonitor', {
99481
99482
99483
99484
99485 constructor: function (config) {
99486 return new Ext.util.paintmonitor.CssAnimation(config);
99487 }
99488 });
99489
99490 /**
99491 * @private
99492 */
99493 Ext.define('Ext.event.publisher.ElementPaint', {
99494
99495 extend: Ext.event.publisher.Publisher ,
99496
99497
99498
99499
99500
99501
99502 targetType: 'element',
99503
99504 handledEvents: ['painted'],
99505
99506 constructor: function() {
99507 this.monitors = {};
99508
99509 this.callSuper(arguments);
99510 },
99511
99512 subscribe: function(target) {
99513 var match = target.match(this.idSelectorRegex),
99514 subscribers = this.subscribers,
99515 id, element;
99516
99517 if (!match) {
99518 return false;
99519 }
99520
99521 id = match[1];
99522
99523 if (subscribers.hasOwnProperty(id)) {
99524 subscribers[id]++;
99525 return true;
99526 }
99527
99528 subscribers[id] = 1;
99529
99530 element = Ext.get(id);
99531
99532 this.monitors[id] = new Ext.util.PaintMonitor({
99533 element: element,
99534 callback: this.onElementPainted,
99535 scope: this,
99536 args: [target, element]
99537 });
99538
99539 return true;
99540 },
99541
99542 unsubscribe: function(target, eventName, all) {
99543 var match = target.match(this.idSelectorRegex),
99544 subscribers = this.subscribers,
99545 id;
99546
99547 if (!match) {
99548 return false;
99549 }
99550
99551 id = match[1];
99552
99553 if (!subscribers.hasOwnProperty(id) || (!all && --subscribers[id] > 0)) {
99554 return true;
99555 }
99556
99557 delete subscribers[id];
99558
99559 this.monitors[id].destroy();
99560 delete this.monitors[id];
99561
99562 return true;
99563 },
99564
99565 onElementPainted: function(target, element) {
99566 Ext.TaskQueue.requestRead('dispatch', this, [target, 'painted', [element]]);
99567 }
99568 });
99569
99570 /**
99571 *
99572 */
99573 Ext.define('Ext.mixin.Templatable', {
99574 extend: Ext.mixin.Mixin ,
99575
99576 mixinConfig: {
99577 id: 'templatable'
99578 },
99579
99580 referenceAttributeName: 'reference',
99581
99582 referenceSelector: '[reference]',
99583
99584 getElementConfig: function() {
99585 return {
99586 reference: 'element'
99587 };
99588 },
99589
99590 getElementTemplate: function() {
99591 var elementTemplate = document.createDocumentFragment();
99592 elementTemplate.appendChild(Ext.Element.create(this.getElementConfig(), true));
99593 return elementTemplate;
99594 },
99595
99596 initElement: function() {
99597 var prototype = this.self.prototype;
99598
99599 prototype.elementTemplate = this.getElementTemplate();
99600 prototype.initElement = prototype.doInitElement;
99601
99602 this.initElement.apply(this, arguments);
99603 },
99604
99605 linkElement: function(reference, node) {
99606 this.link(reference, node);
99607 },
99608
99609 doInitElement: function() {
99610 var referenceAttributeName = this.referenceAttributeName,
99611 renderElement, referenceNodes, i, ln, referenceNode, reference;
99612
99613 renderElement = this.elementTemplate.cloneNode(true);
99614 referenceNodes = renderElement.querySelectorAll(this.referenceSelector);
99615
99616 for (i = 0,ln = referenceNodes.length; i < ln; i++) {
99617 referenceNode = referenceNodes[i];
99618 reference = referenceNode.getAttribute(referenceAttributeName);
99619 referenceNode.removeAttribute(referenceAttributeName);
99620 this.linkElement(reference, referenceNode);
99621 }
99622 }
99623 });
99624
99625 /**
99626 * @private
99627 */
99628 Ext.define('Ext.util.sizemonitor.Abstract', {
99629
99630 mixins: [ Ext.mixin.Templatable ],
99631
99632
99633
99634
99635
99636 config: {
99637 element: null,
99638
99639 callback: Ext.emptyFn,
99640
99641 scope: null,
99642
99643 args: []
99644 },
99645
99646 width: 0,
99647
99648 height: 0,
99649
99650 contentWidth: 0,
99651
99652 contentHeight: 0,
99653
99654 constructor: function(config) {
99655 this.refresh = Ext.Function.bind(this.refresh, this);
99656
99657 this.info = {
99658 width: 0,
99659 height: 0,
99660 contentWidth: 0,
99661 contentHeight: 0,
99662 flag: 0
99663 };
99664
99665 this.initElement();
99666
99667 this.initConfig(config);
99668
99669 this.bindListeners(true);
99670 },
99671
99672 bindListeners: Ext.emptyFn,
99673
99674 applyElement: function(element) {
99675 if (element) {
99676 return Ext.get(element);
99677 }
99678 },
99679
99680 updateElement: function(element) {
99681 element.append(this.detectorsContainer);
99682 element.addCls('x-size-monitored');
99683 },
99684
99685 applyArgs: function(args) {
99686 return args.concat([this.info]);
99687 },
99688
99689 refreshMonitors: Ext.emptyFn,
99690
99691 forceRefresh: function() {
99692 Ext.TaskQueue.requestRead('refresh', this);
99693 },
99694
99695 getContentBounds: function() {
99696 return this.detectorsContainer.getBoundingClientRect();
99697 },
99698
99699 getContentWidth: function() {
99700 return this.detectorsContainer.offsetWidth;
99701 },
99702
99703 getContentHeight: function() {
99704 return this.detectorsContainer.offsetHeight;
99705 },
99706
99707 refreshSize: function() {
99708 var element = this.getElement();
99709
99710 if (!element || element.isDestroyed) {
99711 return false;
99712 }
99713
99714 var width = element.getWidth(),
99715 height = element.getHeight(),
99716 contentWidth = this.getContentWidth(),
99717 contentHeight = this.getContentHeight(),
99718 currentContentWidth = this.contentWidth,
99719 currentContentHeight = this.contentHeight,
99720 info = this.info,
99721 resized = false,
99722 flag;
99723
99724 this.width = width;
99725 this.height = height;
99726 this.contentWidth = contentWidth;
99727 this.contentHeight = contentHeight;
99728
99729 flag = ((currentContentWidth !== contentWidth ? 1 : 0) + (currentContentHeight !== contentHeight ? 2 : 0));
99730
99731 if (flag > 0) {
99732 info.width = width;
99733 info.height = height;
99734 info.contentWidth = contentWidth;
99735 info.contentHeight = contentHeight;
99736 info.flag = flag;
99737
99738 resized = true;
99739 this.getCallback().apply(this.getScope(), this.getArgs());
99740 }
99741
99742 return resized;
99743 },
99744
99745 refresh: function(force) {
99746 if (this.refreshSize() || force) {
99747 Ext.TaskQueue.requestWrite('refreshMonitors', this);
99748 }
99749 },
99750
99751 destroy: function() {
99752 var element = this.getElement();
99753
99754 this.bindListeners(false);
99755
99756 if (element && !element.isDestroyed) {
99757 element.removeCls('x-size-monitored');
99758 }
99759
99760 delete this._element;
99761
99762 this.callSuper();
99763 }
99764 });
99765
99766 /**
99767 * @private
99768 */
99769 Ext.define('Ext.util.sizemonitor.Default', {
99770 extend: Ext.util.sizemonitor.Abstract ,
99771
99772 updateElement: function(element) {},
99773
99774 bindListeners: function(bind) {
99775 var element = this.getElement().dom;
99776
99777 if (!element) {
99778 return;
99779 }
99780
99781 if (bind) {
99782 element.onresize = this.refresh;
99783 }
99784 else {
99785 delete element.onresize;
99786 }
99787 },
99788
99789 getContentBounds: function() {
99790 return this.getElement().dom.getBoundingClientRect();
99791 },
99792
99793 getContentWidth: function() {
99794 return this.getElement().getWidth();
99795 },
99796
99797 getContentHeight: function() {
99798 return this.getElement().getHeight();
99799 }
99800 });
99801
99802 /**
99803 * @private
99804 */
99805 Ext.define('Ext.util.sizemonitor.Scroll', {
99806
99807 extend: Ext.util.sizemonitor.Abstract ,
99808
99809 getElementConfig: function() {
99810 return {
99811 reference: 'detectorsContainer',
99812 classList: ['x-size-monitors', 'scroll'],
99813 children: [
99814 {
99815 reference: 'expandMonitor',
99816 className: 'expand'
99817 },
99818 {
99819 reference: 'shrinkMonitor',
99820 className: 'shrink'
99821 }
99822 ]
99823 }
99824 },
99825
99826 constructor: function(config) {
99827 this.onScroll = Ext.Function.bind(this.onScroll, this);
99828
99829 this.callSuper(arguments);
99830 },
99831
99832 bindListeners: function(bind) {
99833 var method = bind ? 'addEventListener' : 'removeEventListener';
99834
99835 this.expandMonitor[method]('scroll', this.onScroll, true);
99836 this.shrinkMonitor[method]('scroll', this.onScroll, true);
99837 },
99838
99839 forceRefresh: function() {
99840 Ext.TaskQueue.requestRead('refresh', this, [true]);
99841 },
99842
99843 onScroll: function() {
99844 Ext.TaskQueue.requestRead('refresh', this);
99845 },
99846
99847 refreshMonitors: function() {
99848 var expandMonitor = this.expandMonitor,
99849 shrinkMonitor = this.shrinkMonitor,
99850 end = 1000000;
99851
99852 if (expandMonitor && !expandMonitor.isDestroyed) {
99853 expandMonitor.scrollLeft = end;
99854 expandMonitor.scrollTop = end;
99855 }
99856
99857 if (shrinkMonitor && !shrinkMonitor.isDestroyed) {
99858 shrinkMonitor.scrollLeft = end;
99859 shrinkMonitor.scrollTop = end;
99860 }
99861 }
99862 });
99863
99864 /**
99865 * @private
99866 */
99867 Ext.define('Ext.util.sizemonitor.OverflowChange', {
99868
99869 extend: Ext.util.sizemonitor.Abstract ,
99870
99871 constructor: function(config) {
99872 this.onExpand = Ext.Function.bind(this.onExpand, this);
99873 this.onShrink = Ext.Function.bind(this.onShrink, this);
99874
99875 this.callSuper(arguments);
99876 },
99877
99878 getElementConfig: function() {
99879 return {
99880 reference: 'detectorsContainer',
99881 classList: ['x-size-monitors', 'overflowchanged'],
99882 children: [
99883 {
99884 reference: 'expandMonitor',
99885 className: 'expand',
99886 children: [{
99887 reference: 'expandHelper'
99888 }]
99889 },
99890 {
99891 reference: 'shrinkMonitor',
99892 className: 'shrink',
99893 children: [{
99894 reference: 'shrinkHelper'
99895 }]
99896 }
99897 ]
99898 };
99899 },
99900
99901 bindListeners: function(bind) {
99902 var method = bind ? 'addEventListener' : 'removeEventListener';
99903
99904 this.expandMonitor[method](Ext.browser.is.Firefox ? 'underflow' : 'overflowchanged', this.onExpand, true);
99905 this.shrinkMonitor[method](Ext.browser.is.Firefox ? 'overflow' : 'overflowchanged', this.onShrink, true);
99906 },
99907
99908 onExpand: function(e) {
99909 if (Ext.browser.is.Webkit && e.horizontalOverflow && e.verticalOverflow) {
99910 return;
99911 }
99912
99913 Ext.TaskQueue.requestRead('refresh', this);
99914 },
99915
99916 onShrink: function(e) {
99917 if (Ext.browser.is.Webkit && !e.horizontalOverflow && !e.verticalOverflow) {
99918 return;
99919 }
99920
99921 Ext.TaskQueue.requestRead('refresh', this);
99922 },
99923
99924 refreshMonitors: function() {
99925 if (this.isDestroyed) {
99926 return;
99927 }
99928
99929 var expandHelper = this.expandHelper,
99930 shrinkHelper = this.shrinkHelper,
99931 contentBounds = this.getContentBounds(),
99932 width = contentBounds.width,
99933 height = contentBounds.height,
99934 style;
99935
99936 if (expandHelper && !expandHelper.isDestroyed) {
99937 style = expandHelper.style;
99938 style.width = (width + 1) + 'px';
99939 style.height = (height + 1) + 'px';
99940 }
99941
99942 if (shrinkHelper && !shrinkHelper.isDestroyed) {
99943 style = shrinkHelper.style;
99944 style.width = width + 'px';
99945 style.height = height + 'px';
99946 }
99947
99948 Ext.TaskQueue.requestRead('refresh', this);
99949 }
99950 });
99951
99952 /**
99953 *
99954 */
99955 Ext.define('Ext.util.SizeMonitor', {
99956
99957
99958
99959
99960
99961
99962 constructor: function(config) {
99963 var namespace = Ext.util.sizemonitor;
99964
99965 if (Ext.browser.is.Firefox) {
99966 return new namespace.OverflowChange(config);
99967 }
99968 else if (Ext.browser.is.WebKit || Ext.browser.is.IE11) {
99969 return new namespace.Scroll(config);
99970 }
99971 else {
99972 return new namespace.Default(config);
99973 }
99974 }
99975 });
99976
99977 /**
99978 * @private
99979 */
99980 Ext.define('Ext.event.publisher.ElementSize', {
99981
99982 extend: Ext.event.publisher.Publisher ,
99983
99984
99985
99986
99987
99988 targetType: 'element',
99989
99990 handledEvents: ['resize'],
99991
99992 constructor: function() {
99993 this.monitors = {};
99994
99995 this.callSuper(arguments);
99996 },
99997
99998 subscribe: function(target) {
99999 var match = target.match(this.idSelectorRegex),
100000 subscribers = this.subscribers,
100001 id, element, sizeMonitor;
100002
100003 if (!match) {
100004 return false;
100005 }
100006
100007 id = match[1];
100008
100009 if (subscribers.hasOwnProperty(id)) {
100010 subscribers[id]++;
100011 return true;
100012 }
100013
100014 subscribers[id] = 1;
100015
100016 element = Ext.get(id);
100017
100018 this.monitors[id] = sizeMonitor = new Ext.util.SizeMonitor({
100019 element: element,
100020 callback: this.onElementResize,
100021 scope: this,
100022 args: [target, element]
100023 });
100024
100025 this.dispatcher.addListener('element', target, 'painted', 'forceRefresh', sizeMonitor);
100026
100027 return true;
100028 },
100029
100030 unsubscribe: function(target, eventName, all) {
100031 var match = target.match(this.idSelectorRegex),
100032 subscribers = this.subscribers,
100033 monitors = this.monitors,
100034 id, sizeMonitor;
100035
100036 if (!match) {
100037 return false;
100038 }
100039
100040 id = match[1];
100041
100042 if (!subscribers.hasOwnProperty(id) || (!all && --subscribers[id] > 0)) {
100043 return true;
100044 }
100045
100046 delete subscribers[id];
100047
100048 sizeMonitor = monitors[id];
100049
100050 this.dispatcher.removeListener('element', target, 'painted', 'forceRefresh', sizeMonitor);
100051
100052 sizeMonitor.destroy();
100053 delete monitors[id];
100054
100055 return true;
100056 },
100057
100058 onElementResize: function(target, element, info) {
100059 Ext.TaskQueue.requestRead('dispatch', this, [target, 'resize', [element, info]]);
100060 }
100061 });
100062
100063 /**
100064 * @private
100065 */
100066 Ext.define('Ext.event.publisher.TouchGesture', {
100067
100068 extend: Ext.event.publisher.Dom ,
100069
100070
100071
100072
100073
100074
100075
100076 isNotPreventable: /^(select|a)$/i,
100077
100078 handledEvents: ['touchstart', 'touchmove', 'touchend', 'touchcancel'],
100079
100080 mouseToTouchMap: {
100081 mousedown: 'touchstart',
100082 mousemove: 'touchmove',
100083 mouseup: 'touchend'
100084 },
100085
100086 lastEventType: null,
100087
100088 config: {
100089 moveThrottle: 0,
100090 recognizers: {}
100091 },
100092
100093 constructor: function(config) {
100094 var me = this;
100095
100096 this.eventProcessors = {
100097 touchstart: this.onTouchStart,
100098 touchmove: this.onTouchMove,
100099 touchend: this.onTouchEnd,
100100 touchcancel: this.onTouchEnd
100101 };
100102
100103 this.eventToRecognizerMap = {};
100104
100105 this.activeRecognizers = [];
100106
100107 this.touchesMap = {};
100108
100109 this.currentIdentifiers = [];
100110
100111 if (Ext.browser.is.Chrome && Ext.os.is.Android) {
100112 this.screenPositionRatio = Ext.browser.version.gt('18') ? 1 : 1 / window.devicePixelRatio;
100113 }
100114 else if (Ext.browser.is.AndroidStock4) {
100115 this.screenPositionRatio = 1;
100116 }
100117 else if (Ext.os.is.BlackBerry) {
100118 this.screenPositionRatio = 1 / window.devicePixelRatio;
100119 }
100120 else if (Ext.browser.engineName == 'WebKit' && Ext.os.is.Desktop) {
100121 this.screenPositionRatio = 1;
100122 }
100123 else {
100124 this.screenPositionRatio = window.innerWidth / window.screen.width;
100125 }
100126 this.initConfig(config);
100127
100128 if (Ext.feature.has.Touch) {
100129 // bind handlers that are only invoked when the browser has touchevents
100130 me.onTargetTouchMove = me.onTargetTouchMove.bind(me);
100131 me.onTargetTouchEnd = me.onTargetTouchEnd.bind(me);
100132 }
100133
100134 return this.callSuper();
100135 },
100136
100137 applyRecognizers: function(recognizers) {
100138 var i, recognizer;
100139
100140 for (i in recognizers) {
100141 if (recognizers.hasOwnProperty(i)) {
100142 recognizer = recognizers[i];
100143
100144 if (recognizer) {
100145 this.registerRecognizer(recognizer);
100146 }
100147 }
100148 }
100149
100150 return recognizers;
100151 },
100152
100153 handles: function(eventName) {
100154 return this.callSuper(arguments) || this.eventToRecognizerMap.hasOwnProperty(eventName);
100155 },
100156
100157 doesEventBubble: function() {
100158 // All touch events bubble
100159 return true;
100160 },
100161 onEvent: function(e) {
100162 var type = e.type,
100163 lastEventType = this.lastEventType,
100164 touchList = [e];
100165
100166 if (this.eventProcessors[type]) {
100167 this.eventProcessors[type].call(this, e);
100168 return;
100169 }
100170
100171 if ('button' in e && e.button > 0) {
100172 return;
100173 }
100174 else {
100175 // Temporary fix for a recent Chrome bugs where events don't seem to bubble up to document
100176 // when the element is being animated with webkit-transition (2 mousedowns without any mouseup)
100177 if (type === 'mousedown' && lastEventType && lastEventType !== 'mouseup') {
100178 var fixedEvent = document.createEvent("MouseEvent");
100179 fixedEvent.initMouseEvent('mouseup', e.bubbles, e.cancelable,
100180 document.defaultView, e.detail, e.screenX, e.screenY, e.clientX,
100181 e.clientY, e.ctrlKey, e.altKey, e.shiftKey, e.metaKey, e.metaKey,
100182 e.button, e.relatedTarget);
100183
100184 this.onEvent(fixedEvent);
100185 }
100186
100187 if (type !== 'mousemove') {
100188 this.lastEventType = type;
100189 }
100190
100191 e.identifier = 1;
100192 e.touches = (type !== 'mouseup') ? touchList : [];
100193 e.targetTouches = (type !== 'mouseup') ? touchList : [];
100194 e.changedTouches = touchList;
100195
100196 this.eventProcessors[this.mouseToTouchMap[type]].call(this, e);
100197 }
100198 },
100199
100200 registerRecognizer: function(recognizer) {
100201 var map = this.eventToRecognizerMap,
100202 activeRecognizers = this.activeRecognizers,
100203 handledEvents = recognizer.getHandledEvents(),
100204 i, ln, eventName;
100205
100206 recognizer.setOnRecognized(this.onRecognized);
100207 recognizer.setCallbackScope(this);
100208
100209 for (i = 0,ln = handledEvents.length; i < ln; i++) {
100210 eventName = handledEvents[i];
100211
100212 map[eventName] = recognizer;
100213 }
100214
100215 activeRecognizers.push(recognizer);
100216
100217 return this;
100218 },
100219
100220 onRecognized: function(eventName, e, touches, info) {
100221 var targetGroups = [],
100222 ln = touches.length,
100223 targets, i, touch;
100224
100225 if (ln === 1) {
100226 return this.publish(eventName, touches[0].targets, e, info);
100227 }
100228
100229 for (i = 0; i < ln; i++) {
100230 touch = touches[i];
100231 targetGroups.push(touch.targets);
100232 }
100233
100234 targets = this.getCommonTargets(targetGroups);
100235
100236 this.publish(eventName, targets, e, info);
100237 },
100238
100239 publish: function(eventName, targets, event, info) {
100240 event.set(info);
100241 return this.callSuper([eventName, targets, event]);
100242 },
100243
100244 getCommonTargets: function(targetGroups) {
100245 var firstTargetGroup = targetGroups[0],
100246 ln = targetGroups.length;
100247
100248 if (ln === 1) {
100249 return firstTargetGroup;
100250 }
100251
100252 var commonTargets = [],
100253 i = 1,
100254 target, targets, j;
100255
100256 while (true) {
100257 target = firstTargetGroup[firstTargetGroup.length - i];
100258
100259 if (!target) {
100260 return commonTargets;
100261 }
100262
100263 for (j = 1; j < ln; j++) {
100264 targets = targetGroups[j];
100265
100266 if (targets[targets.length - i] !== target) {
100267 return commonTargets;
100268 }
100269 }
100270
100271 commonTargets.unshift(target);
100272 i++;
100273 }
100274
100275 return commonTargets;
100276 },
100277
100278 invokeRecognizers: function(methodName, e) {
100279 var recognizers = this.activeRecognizers,
100280 ln = recognizers.length,
100281 i, recognizer;
100282
100283 if (methodName === 'onStart') {
100284 for (i = 0; i < ln; i++) {
100285 recognizers[i].isActive = true;
100286 }
100287 }
100288
100289 for (i = 0; i < ln; i++) {
100290 recognizer = recognizers[i];
100291 if (recognizer.isActive && recognizer[methodName].call(recognizer, e) === false) {
100292 recognizer.isActive = false;
100293 }
100294 }
100295 },
100296
100297 getActiveRecognizers: function() {
100298 return this.activeRecognizers;
100299 },
100300
100301 updateTouch: function(touch) {
100302 var identifier = touch.identifier,
100303 currentTouch = this.touchesMap[identifier],
100304 target, x, y;
100305
100306 if (!currentTouch) {
100307 target = this.getElementTarget(touch.target);
100308
100309 this.touchesMap[identifier] = currentTouch = {
100310 identifier: identifier,
100311 target: target,
100312 targets: this.getBubblingTargets(target)
100313 };
100314
100315 this.currentIdentifiers.push(identifier);
100316 }
100317
100318 x = touch.pageX;
100319 y = touch.pageY;
100320
100321 if (x === currentTouch.pageX && y === currentTouch.pageY) {
100322 return false;
100323 }
100324
100325 currentTouch.pageX = x;
100326 currentTouch.pageY = y;
100327 currentTouch.timeStamp = touch.timeStamp;
100328 currentTouch.point = new Ext.util.Point(x, y);
100329
100330 return currentTouch;
100331 },
100332
100333 updateTouches: function(touches) {
100334 var i, ln, touch,
100335 changedTouches = [];
100336
100337 for (i = 0, ln = touches.length; i < ln; i++) {
100338 touch = this.updateTouch(touches[i]);
100339 if (touch) {
100340 changedTouches.push(touch);
100341 }
100342 }
100343
100344 return changedTouches;
100345 },
100346
100347 syncTouches: function (touches) {
100348 var touchIDs = [], len = touches.length,
100349 i, id, touch, ghostTouches;
100350
100351 // Collect the actual touch IDs that exist
100352 for (i = 0; i < len; i++) {
100353 touch = touches[i];
100354 touchIDs.push(touch.identifier);
100355 }
100356
100357 // Compare actual IDs to cached IDs
100358 // Remove any that are not real anymore
100359 ghostTouches = Ext.Array.difference(this.currentIdentifiers, touchIDs);
100360 len = ghostTouches.length;
100361
100362 for (i = 0; i < len; i++) {
100363 id = ghostTouches[i];
100364 Ext.Array.remove(this.currentIdentifiers, id);
100365 delete this.touchesMap[id];
100366 }
100367 },
100368
100369 factoryEvent: function(e) {
100370 return new Ext.event.Touch(e, null, this.touchesMap, this.currentIdentifiers);
100371 },
100372
100373 onTouchStart: function(e) {
100374 var changedTouches = e.changedTouches,
100375 target = e.target,
100376 touches = e.touches,
100377 ln = changedTouches.length,
100378 isNotPreventable = this.isNotPreventable,
100379 isTouch = (e.type === 'touchstart'),
100380 me = this,
100381 i, touch, parent;
100382
100383 // Potentially sync issue from various reasons.
100384 // example: ios8 does not dispatch touchend on audio element play/pause tap.
100385 if (touches && touches.length < this.currentIdentifiers.length + 1) {
100386 this.syncTouches(touches);
100387 }
100388
100389 this.updateTouches(changedTouches);
100390
100391 e = this.factoryEvent(e);
100392 changedTouches = e.changedTouches;
100393
100394 // TOUCH-3934
100395 // Android event system will not dispatch touchend for any multitouch
100396 // event that has not been preventDefaulted.
100397 if(Ext.browser.is.AndroidStock && this.currentIdentifiers.length >= 2) {
100398 e.preventDefault();
100399 }
100400
100401 // If targets are destroyed while touches are active on them
100402 // we need these listeners to sync up our internal TouchesMap
100403 if (isTouch) {
100404 target.addEventListener('touchmove', me.onTargetTouchMove);
100405 target.addEventListener('touchend', me.onTargetTouchEnd);
100406 target.addEventListener('touchcancel', me.onTargetTouchEnd);
100407 }
100408
100409 for (i = 0; i < ln; i++) {
100410 touch = changedTouches[i];
100411 this.publish('touchstart', touch.targets, e, {touch: touch});
100412 }
100413
100414 if (!this.isStarted) {
100415 this.isStarted = true;
100416 this.invokeRecognizers('onStart', e);
100417 }
100418
100419 this.invokeRecognizers('onTouchStart', e);
100420
100421 parent = target.parentNode || {};
100422 },
100423
100424 onTouchMove: function(e) {
100425 if (!this.isStarted) {
100426 return;
100427 }
100428
100429 if (!this.animationQueued) {
100430 this.animationQueued = true;
100431 Ext.AnimationQueue.start('onAnimationFrame', this);
100432 }
100433
100434 this.lastMoveEvent = e;
100435 },
100436
100437 onAnimationFrame: function() {
100438 var event = this.lastMoveEvent;
100439
100440 if (event) {
100441 this.lastMoveEvent = null;
100442 this.doTouchMove(event);
100443 }
100444 },
100445
100446 doTouchMove: function(e) {
100447 var changedTouches, i, ln, touch;
100448
100449 changedTouches = this.updateTouches(e.changedTouches);
100450
100451 ln = changedTouches.length;
100452
100453 e = this.factoryEvent(e);
100454
100455 for (i = 0; i < ln; i++) {
100456 touch = changedTouches[i];
100457 this.publish('touchmove', touch.targets, e, {touch: touch});
100458 }
100459
100460 if (ln > 0) {
100461 this.invokeRecognizers('onTouchMove', e);
100462 }
100463 },
100464
100465 onTouchEnd: function(e) {
100466 if (!this.isStarted) {
100467 return;
100468 }
100469
100470 if (this.lastMoveEvent) {
100471 this.onAnimationFrame();
100472 }
100473
100474 var touchesMap = this.touchesMap,
100475 currentIdentifiers = this.currentIdentifiers,
100476 changedTouches = e.changedTouches,
100477 ln = changedTouches.length,
100478 identifier, i, touch;
100479
100480 this.updateTouches(changedTouches);
100481
100482 changedTouches = e.changedTouches;
100483
100484 for (i = 0; i < ln; i++) {
100485 Ext.Array.remove(currentIdentifiers, changedTouches[i].identifier);
100486 }
100487
100488 e = this.factoryEvent(e);
100489
100490 for (i = 0; i < ln; i++) {
100491 identifier = changedTouches[i].identifier;
100492 touch = touchesMap[identifier];
100493 delete touchesMap[identifier];
100494 this.publish('touchend', touch.targets, e, {touch: touch});
100495 }
100496
100497 this.invokeRecognizers('onTouchEnd', e);
100498
100499 // This previously was set to e.touches.length === 1 to catch errors in syncing
100500 // this has since been addressed to keep proper sync and now this is a catch for
100501 // a sync error in touches to reset our internal maps
100502 if (e.touches && e.touches.length === 0 && currentIdentifiers.length) {
100503 currentIdentifiers.length = 0;
100504 this.touchesMap = {};
100505 }
100506
100507 if (currentIdentifiers.length === 0) {
100508 this.isStarted = false;
100509 this.invokeRecognizers('onEnd', e);
100510 if (this.animationQueued) {
100511 this.animationQueued = false;
100512 Ext.AnimationQueue.stop('onAnimationFrame', this);
100513 }
100514 }
100515 },
100516
100517 onTargetTouchMove: function(e) {
100518 if (!Ext.getBody().contains(e.target)) {
100519 this.onTouchMove(e);
100520 }
100521 },
100522
100523 onTargetTouchEnd: function(e) {
100524 var me = this,
100525 target = e.target,
100526 touchCount=0,
100527 touchTarget;
100528
100529 // Determine how many active touches there are on this target
100530 for (identifier in this.touchesMap) {
100531 touchTarget = this.touchesMap[identifier].target;
100532 if (touchTarget === target ) {
100533 touchCount++;
100534 }
100535 }
100536
100537 // If this is the last active touch on the target remove the target listeners
100538 if (touchCount <= 1) {
100539 target.removeEventListener('touchmove', me.onTargetTouchMove);
100540 target.removeEventListener('touchend', me.onTargetTouchEnd);
100541 target.removeEventListener('touchcancel', me.onTargetTouchEnd);
100542 }
100543
100544 if (!Ext.getBody().contains(target)) {
100545 me.onTouchEnd(e);
100546 }
100547 }
100548
100549 }, function() {
100550 if (Ext.feature.has.Pointer) {
100551 this.override({
100552 pointerToTouchMap: {
100553 MSPointerDown: 'touchstart',
100554 MSPointerMove: 'touchmove',
100555 MSPointerUp: 'touchend',
100556 MSPointerCancel: 'touchcancel',
100557 pointerdown: 'touchstart',
100558 pointermove: 'touchmove',
100559 pointerup: 'touchend',
100560 pointercancel: 'touchcancel'
100561 },
100562
100563 touchToPointerMap: {
100564 touchstart: 'MSPointerDown',
100565 touchmove: 'MSPointerMove',
100566 touchend: 'MSPointerUp',
100567 touchcancel: 'MSPointerCancel'
100568 },
100569
100570 attachListener: function(eventName, doc) {
100571 eventName = this.touchToPointerMap[eventName];
100572
100573 if (!eventName) {
100574 return;
100575 }
100576
100577 return this.callOverridden([eventName, doc]);
100578 },
100579
100580 onEvent: function(e) {
100581 var type = e.type;
100582 if (
100583 this.currentIdentifiers.length === 0 &&
100584 // This is for IE 10 and IE 11
100585 (e.pointerType === e.MSPOINTER_TYPE_TOUCH || e.pointerType === "touch") &&
100586 // This is for IE 10 and IE 11
100587 (type === "MSPointerMove" || type === "pointermove")
100588 ) {
100589 type = "MSPointerDown";
100590 }
100591
100592 if ('button' in e && e.button > 0) {
100593 return;
100594 }
100595
100596 type = this.pointerToTouchMap[type];
100597 e.identifier = e.pointerId;
100598 e.changedTouches = [e];
100599
100600 this.eventProcessors[type].call(this, e);
100601 }
100602 });
100603 }
100604 else if (!Ext.browser.is.Ripple && (Ext.os.is.ChromeOS || !Ext.feature.has.Touch)) {
100605 this.override({
100606 handledEvents: ['touchstart', 'touchmove', 'touchend', 'touchcancel', 'mousedown', 'mousemove', 'mouseup']
100607 });
100608 }
100609 });
100610
100611 /**
100612 * A base class for all event recognizers in Sencha Touch.
100613 *
100614 * Sencha Touch, by default, includes various different {@link Ext.event.recognizer.Recognizer} subclasses to recognize
100615 * events happening in your application.
100616 *
100617 * ## Default recognizers
100618 *
100619 * * {@link Ext.event.recognizer.Tap}
100620 * * {@link Ext.event.recognizer.DoubleTap}
100621 * * {@link Ext.event.recognizer.LongPress}
100622 * * {@link Ext.event.recognizer.Drag}
100623 * * {@link Ext.event.recognizer.HorizontalSwipe}
100624 * * {@link Ext.event.recognizer.Pinch}
100625 * * {@link Ext.event.recognizer.Rotate}
100626 *
100627 * ## Additional recognizers
100628 *
100629 * * {@link Ext.event.recognizer.VerticalSwipe}
100630 *
100631 * If you want to create custom recognizers, or disable recognizers in your Sencha Touch application, please refer to the
100632 * documentation in {@link Ext#setup}.
100633 *
100634 * @private
100635 */
100636 Ext.define('Ext.event.recognizer.Recognizer', {
100637 mixins: [ Ext.mixin.Identifiable ],
100638
100639 handledEvents: [],
100640
100641 config: {
100642 onRecognized: Ext.emptyFn,
100643 onFailed: Ext.emptyFn,
100644 callbackScope: null
100645 },
100646
100647 constructor: function(config) {
100648 this.initConfig(config);
100649
100650 return this;
100651 },
100652
100653 getHandledEvents: function() {
100654 return this.handledEvents;
100655 },
100656
100657 onStart: Ext.emptyFn,
100658
100659 onEnd: Ext.emptyFn,
100660
100661 fail: function() {
100662 this.getOnFailed().apply(this.getCallbackScope(), arguments);
100663
100664 return false;
100665 },
100666
100667 fire: function() {
100668 this.getOnRecognized().apply(this.getCallbackScope(), arguments);
100669 }
100670 });
100671
100672 /**
100673 * @private
100674 */
100675 Ext.define('Ext.event.recognizer.Touch', {
100676
100677 extend: Ext.event.recognizer.Recognizer ,
100678
100679 onTouchStart: Ext.emptyFn,
100680
100681 onTouchMove: Ext.emptyFn,
100682
100683 onTouchEnd: Ext.emptyFn
100684 });
100685
100686 /**
100687 * @private
100688 */
100689 Ext.define('Ext.event.recognizer.SingleTouch', {
100690 extend: Ext.event.recognizer.Touch ,
100691
100692 inheritableStatics: {
100693 NOT_SINGLE_TOUCH: 0x01,
100694 TOUCH_MOVED: 0x02
100695 },
100696
100697 onTouchStart: function(e) {
100698 if (e.touches.length > 1) {
100699 return this.fail(this.self.NOT_SINGLE_TOUCH);
100700 }
100701 }
100702 });
100703
100704
100705 /**
100706 * A simple event recognizer which knows when you double tap.
100707 *
100708 * @private
100709 */
100710 Ext.define('Ext.event.recognizer.DoubleTap', {
100711
100712 extend: Ext.event.recognizer.SingleTouch ,
100713
100714 inheritableStatics: {
100715 DIFFERENT_TARGET: 0x03
100716 },
100717
100718 config: {
100719 maxDuration: 300
100720 },
100721
100722 handledEvents: ['singletap', 'doubletap'],
100723
100724 /**
100725 * @member Ext.dom.Element
100726 * @event singletap
100727 * Fires when there is a single tap.
100728 * @param {Ext.event.Event} event The {@link Ext.event.Event} event encapsulating the DOM event.
100729 * @param {HTMLElement} node The target of the event.
100730 * @param {Object} options The options object passed to Ext.mixin.Observable.addListener.
100731 */
100732
100733 /**
100734 * @member Ext.dom.Element
100735 * @event doubletap
100736 * Fires when there is a double tap.
100737 * @param {Ext.event.Event} event The {@link Ext.event.Event} event encapsulating the DOM event.
100738 * @param {HTMLElement} node The target of the event.
100739 * @param {Object} options The options object passed to Ext.mixin.Observable.addListener.
100740 */
100741
100742 singleTapTimer: null,
100743
100744 startTime: 0,
100745
100746 lastTapTime: 0,
100747
100748 onTouchStart: function(e) {
100749 if (this.callParent(arguments) === false) {
100750 return false;
100751 }
100752
100753 this.startTime = e.time;
100754
100755 clearTimeout(this.singleTapTimer);
100756 },
100757
100758 onTouchMove: function() {
100759 return this.fail(this.self.TOUCH_MOVED);
100760 },
100761
100762 onEnd: function(e) {
100763 var me = this,
100764 maxDuration = this.getMaxDuration(),
100765 touch = e.changedTouches[0],
100766 time = e.time,
100767 target = e.target,
100768 lastTapTime = this.lastTapTime,
100769 lastTarget = this.lastTarget,
100770 duration;
100771
100772 this.lastTapTime = time;
100773 this.lastTarget = target;
100774
100775 if (lastTapTime) {
100776 duration = time - lastTapTime;
100777
100778 if (duration <= maxDuration) {
100779 if (target !== lastTarget) {
100780 return this.fail(this.self.DIFFERENT_TARGET);
100781 }
100782
100783 this.lastTarget = null;
100784 this.lastTapTime = 0;
100785
100786 this.fire('doubletap', e, [touch], {
100787 touch: touch,
100788 duration: duration
100789 });
100790
100791 return;
100792 }
100793 }
100794
100795 if (time - this.startTime > maxDuration) {
100796 this.fireSingleTap(e, touch);
100797 }
100798 else {
100799 this.singleTapTimer = setTimeout(function() {
100800 me.fireSingleTap(e, touch);
100801 }, maxDuration);
100802 }
100803 },
100804
100805 fireSingleTap: function(e, touch) {
100806 this.fire('singletap', e, [touch], {
100807 touch: touch
100808 });
100809 }
100810 });
100811
100812 /**
100813 * @private
100814 */
100815 Ext.define('Ext.event.recognizer.Drag', {
100816 extend: Ext.event.recognizer.SingleTouch ,
100817
100818 isStarted: false,
100819
100820 startPoint: null,
100821
100822 previousPoint: null,
100823
100824 lastPoint: null,
100825
100826 handledEvents: ['dragstart', 'drag', 'dragend'],
100827
100828 config: {
100829 /**
100830 * @cfg {Number} minDistance
100831 * The minimum distance of pixels before a touch event becomes a drag event.
100832 */
100833 minDistance: 8
100834 },
100835
100836 constructor: function() {
100837 this.callSuper(arguments);
100838
100839 this.info = {
100840 touch: null,
100841 previous: {
100842 x: 0,
100843 y: 0
100844 },
100845 x: 0,
100846 y: 0,
100847 delta: {
100848 x: 0,
100849 y: 0
100850 },
100851 absDelta: {
100852 x: 0,
100853 y: 0
100854 },
100855 flick: {
100856 velocity: {
100857 x: 0,
100858 y: 0
100859 }
100860 },
100861 direction: {
100862 x: 0,
100863 y: 0
100864 },
100865 time: 0,
100866 previousTime: {
100867 x: 0,
100868 y: 0
100869 }
100870 };
100871 },
100872
100873 onTouchStart: function(e) {
100874 if (this.callSuper(arguments) === false) {
100875 if (this.isStarted && this.lastMoveEvent !== null) {
100876 this.lastMoveEvent.isStopped = false;
100877 this.onTouchEnd(this.lastMoveEvent);
100878 }
100879 return false;
100880 }
100881
100882 this.startTime = e.time;
100883 this.startPoint = e.changedTouches[0].point;
100884 },
100885
100886 tryDragStart: function(e) {
100887 var startPoint = this.startPoint,
100888 touches = e.changedTouches,
100889 touch = touches[0],
100890 point = touch.point,
100891 minDistance = this.getMinDistance(),
100892 info = this.info;
100893
100894 if (Math.abs(point.getDistanceTo(startPoint)) >= minDistance) {
100895 this.isStarted = true;
100896
100897 this.previousPoint = this.lastPoint = point;
100898
100899 this.resetInfo('x', e, touch);
100900 this.resetInfo('y', e, touch);
100901
100902 info.time = e.time;
100903
100904 this.fire('dragstart', e, touches, info);
100905 }
100906 },
100907
100908 onTouchMove: function(e) {
100909 if (!this.isStarted) {
100910 this.tryDragStart(e);
100911 }
100912
100913 if (!this.isStarted) {
100914 return;
100915 }
100916
100917 var touches = e.changedTouches,
100918 touch = touches[0],
100919 point = touch.point;
100920
100921 if (this.lastPoint) {
100922 this.previousPoint = this.lastPoint;
100923 }
100924
100925 this.lastPoint = point;
100926 this.lastMoveEvent = e;
100927
100928 this.updateInfo('x', e, touch, true);
100929 this.updateInfo('y', e, touch, true);
100930
100931 this.info.time = e.time;
100932
100933 this.fire('drag', e, touches, this.info);
100934 },
100935
100936 onAxisDragEnd: function(axis, info) {
100937 var duration = info.time - info.previousTime[axis];
100938
100939 if (duration > 0) {
100940 info.flick.velocity[axis] = (info[axis] - info.previous[axis]) / duration;
100941 }
100942 },
100943
100944 resetInfo: function(axis, e, touch) {
100945 var value = this.lastPoint[axis],
100946 startValue = this.startPoint[axis],
100947 delta = value - startValue,
100948 capAxis = axis.toUpperCase(),
100949 info = this.info;
100950
100951 info.touch = touch;
100952
100953 info.delta[axis] = delta;
100954 info.absDelta[axis] = Math.abs(delta);
100955
100956 info.previousTime[axis] = this.startTime;
100957 info.previous[axis] = startValue;
100958 info[axis] = value;
100959 info.direction[axis] = 0;
100960
100961 info['start' + capAxis] = this.startPoint[axis];
100962 info['previous' + capAxis] = info.previous[axis];
100963 info['page' + capAxis] = info[axis];
100964 info['delta' + capAxis] = info.delta[axis];
100965 info['absDelta' + capAxis] = info.absDelta[axis];
100966 info['previousDelta' + capAxis] = 0;
100967 info.startTime = this.startTime;
100968 },
100969
100970 updateInfo: function(axis, e, touch, updatePrevious) {
100971 var time = e.time,
100972 value = this.lastPoint[axis],
100973 previousValue = this.previousPoint[axis],
100974 startValue = this.startPoint[axis],
100975 delta = value - startValue,
100976 info = this.info,
100977 direction = info.direction,
100978 capAxis = axis.toUpperCase(),
100979 previousFlick = info.previous[axis],
100980 previousDelta;
100981
100982 info.touch = touch;
100983
100984 previousDelta = info.delta[axis];
100985 info.delta[axis] = delta;
100986 info.absDelta[axis] = Math.abs(delta);
100987
100988 if (updatePrevious && value !== previousFlick && value !== info[axis] && time - info.previousTime[axis] >= 50) {
100989 info.previous[axis] = info[axis];
100990 info.previousTime[axis] = info.time;
100991 }
100992
100993 info[axis] = value;
100994
100995 if (value > previousValue) {
100996 direction[axis] = 1;
100997 }
100998 else if (value < previousValue) {
100999 direction[axis] = -1;
101000 }
101001
101002 info['start' + capAxis] = this.startPoint[axis];
101003 info['previous' + capAxis] = info.previous[axis];
101004 info['page' + capAxis] = info[axis];
101005 info['delta' + capAxis] = info.delta[axis];
101006 info['absDelta' + capAxis] = info.absDelta[axis];
101007 info['previousDelta' + capAxis] = previousDelta;
101008 info.startTime = this.startTime;
101009 },
101010
101011 onTouchEnd: function(e) {
101012 if (!this.isStarted) {
101013 this.tryDragStart(e);
101014 }
101015
101016 if (this.isStarted) {
101017 var touches = e.changedTouches,
101018 touch = touches[0],
101019 point = touch.point,
101020 info = this.info;
101021
101022 this.isStarted = false;
101023 this.lastPoint = point;
101024
101025 this.updateInfo('x', e, touch);
101026 this.updateInfo('y', e, touch);
101027
101028 info.time = e.time;
101029
101030 this.onAxisDragEnd('x', info);
101031 this.onAxisDragEnd('y', info);
101032
101033 this.fire('dragend', e, touches, info);
101034
101035 this.startPoint = null;
101036 this.previousPoint = null;
101037 this.lastPoint = null;
101038 this.lastMoveEvent = null;
101039 }
101040 }
101041 });
101042
101043 /**
101044 * A base class used for both {@link Ext.event.recognizer.VerticalSwipe} and {@link Ext.event.recognizer.HorizontalSwipe}
101045 * event recognizers.
101046 *
101047 * @private
101048 */
101049 Ext.define('Ext.event.recognizer.Swipe', {
101050 extend: Ext.event.recognizer.SingleTouch ,
101051
101052 handledEvents: ['swipestart', 'swipe'],
101053
101054 /**
101055 * @member Ext.dom.Element
101056 * @event swipe
101057 * Fires when there is a swipe
101058 * When listening to this, ensure you know about the {@link Ext.event.Event#direction} property in the `event` object.
101059 * @param {Ext.event.Event} event The {@link Ext.event.Event} event encapsulating the DOM event.
101060 * @param {HTMLElement} node The target of the event.
101061 * @param {Object} options The options object passed to Ext.mixin.Observable.addListener.
101062 */
101063
101064 /**
101065 * @property {Number} direction
101066 * The direction of the swipe. Available options are:
101067 *
101068 * - up
101069 * - down
101070 * - left
101071 * - right
101072 *
101073 * __Note:__ In order to recognize swiping up and down, you must enable the vertical swipe recognizer.
101074 *
101075 * **This is only available when the event type is `swipe`**
101076 * @member Ext.event.Event
101077 */
101078
101079 /**
101080 * @property {Number} duration
101081 * The duration of the swipe.
101082 *
101083 * **This is only available when the event type is `swipe`**
101084 * @member Ext.event.Event
101085 */
101086
101087 inheritableStatics: {
101088 MAX_OFFSET_EXCEEDED: 0x10,
101089 MAX_DURATION_EXCEEDED: 0x11,
101090 DISTANCE_NOT_ENOUGH: 0x12
101091 },
101092
101093 config: {
101094 minDistance: 80,
101095 maxOffset: 35,
101096 maxDuration: 1000
101097 },
101098
101099 onTouchStart: function(e) {
101100 if (this.callParent(arguments) === false) {
101101 return false;
101102 }
101103
101104 var touch = e.changedTouches[0];
101105
101106 this.startTime = e.time;
101107
101108 this.isHorizontal = true;
101109 this.isVertical = true;
101110
101111 this.startX = touch.pageX;
101112 this.startY = touch.pageY;
101113 },
101114
101115 onTouchMove: function(e) {
101116 var touch = e.changedTouches[0],
101117 x = touch.pageX,
101118 y = touch.pageY,
101119 deltaX = x - this.startX,
101120 deltaY = y - this.startY,
101121 absDeltaX = Math.abs(x - this.startX),
101122 absDeltaY = Math.abs(y - this.startY),
101123 duration = e.time - this.startTime,
101124 minDistance = this.getMinDistance(),
101125 time = e.time,
101126 direction, distance;
101127
101128 if (time - this.startTime > this.getMaxDuration()) {
101129 return this.fail(this.self.MAX_DURATION_EXCEEDED);
101130 }
101131
101132 if (this.isHorizontal && absDeltaY > this.getMaxOffset()) {
101133 this.isHorizontal = false;
101134 }
101135
101136 if (this.isVertical && absDeltaX > this.getMaxOffset()) {
101137 this.isVertical = false;
101138 }
101139
101140 if (!this.isVertical || !this.isHorizontal) {
101141 if (this.isHorizontal && absDeltaX < minDistance) {
101142 direction = (deltaX < 0) ? 'left' : 'right';
101143 distance = absDeltaX;
101144 }
101145 else if (this.isVertical && absDeltaY < minDistance) {
101146 direction = (deltaY < 0) ? 'up' : 'down';
101147 distance = absDeltaY;
101148 }
101149 }
101150
101151 if (direction && !this.started) {
101152 this.started = true;
101153
101154 this.fire('swipestart', e, [touch], {
101155 touch: touch,
101156 direction: direction,
101157 distance: distance,
101158 duration: duration
101159 });
101160 }
101161
101162 if (!this.isHorizontal && !this.isVertical) {
101163 return this.fail(this.self.MAX_OFFSET_EXCEEDED);
101164 }
101165 },
101166
101167 onTouchEnd: function(e) {
101168 if (this.onTouchMove(e) === false) {
101169 return false;
101170 }
101171
101172 var touch = e.changedTouches[0],
101173 x = touch.pageX,
101174 y = touch.pageY,
101175 deltaX = x - this.startX,
101176 deltaY = y - this.startY,
101177 absDeltaX = Math.abs(deltaX),
101178 absDeltaY = Math.abs(deltaY),
101179 minDistance = this.getMinDistance(),
101180 duration = e.time - this.startTime,
101181 direction, distance;
101182
101183 if (this.isVertical && absDeltaY < minDistance) {
101184 this.isVertical = false;
101185 }
101186
101187 if (this.isHorizontal && absDeltaX < minDistance) {
101188 this.isHorizontal = false;
101189 }
101190
101191 if (this.isHorizontal) {
101192 direction = (deltaX < 0) ? 'left' : 'right';
101193 distance = absDeltaX;
101194 }
101195 else if (this.isVertical) {
101196 direction = (deltaY < 0) ? 'up' : 'down';
101197 distance = absDeltaY;
101198 }
101199 else {
101200 return this.fail(this.self.DISTANCE_NOT_ENOUGH);
101201 }
101202
101203 this.started = false;
101204
101205 this.fire('swipe', e, [touch], {
101206 touch: touch,
101207 direction: direction,
101208 distance: distance,
101209 duration: duration
101210 });
101211 }
101212 });
101213
101214 /**
101215 * A event recognizer created to recognize swipe movements from the edge of a container.
101216 *
101217 * @private
101218 */
101219 Ext.define('Ext.event.recognizer.EdgeSwipe', {
101220 extend: Ext.event.recognizer.Swipe ,
101221
101222 handledEvents: [
101223 'edgeswipe',
101224 'edgeswipestart',
101225 'edgeswipeend'
101226 ],
101227
101228 inheritableStatics: {
101229 NOT_NEAR_EDGE: 0x13
101230 },
101231
101232 config: {
101233 minDistance: 60
101234 },
101235
101236 onTouchStart: function(e) {
101237 if (this.callParent(arguments) === false) {
101238 return false;
101239 }
101240
101241 var touch = e.changedTouches[0];
101242
101243 this.started = false;
101244
101245 this.direction = null;
101246
101247 this.isHorizontal = true;
101248 this.isVertical = true;
101249
101250 this.startX = touch.pageX;
101251 this.startY = touch.pageY;
101252 },
101253
101254 onTouchMove: function(e) {
101255 var touch = e.changedTouches[0],
101256 x = touch.pageX,
101257 y = touch.pageY,
101258 deltaX = x - this.startX,
101259 deltaY = y - this.startY,
101260 absDeltaY = Math.abs(y - this.startY),
101261 absDeltaX = Math.abs(x - this.startX),
101262 minDistance = this.getMinDistance(),
101263 maxOffset = this.getMaxOffset(),
101264 duration = e.time - this.startTime,
101265 elementWidth = Ext.Viewport && Ext.Viewport.element.getWidth(),
101266 elementHeight = Ext.Viewport && Ext.Viewport.element.getHeight(),
101267 direction, distance;
101268
101269 // Check if the swipe is going off vertical
101270 if (this.isVertical && absDeltaX > maxOffset) {
101271 this.isVertical = false;
101272 }
101273
101274 // Check if the swipe is going off horizontal
101275 if (this.isHorizontal && absDeltaY > maxOffset) {
101276 this.isHorizontal = false;
101277 }
101278
101279 // If the swipe is both, determin which one it is from the maximum distance travelled
101280 if (this.isVertical && this.isHorizontal) {
101281 if (absDeltaY > absDeltaX) {
101282 this.isHorizontal = false;
101283 } else {
101284 this.isVertical = false;
101285 }
101286 }
101287
101288 // Get the direction of the swipe
101289 if (this.isHorizontal) {
101290 direction = (deltaX < 0) ? 'left' : 'right';
101291 distance = deltaX;
101292 }
101293 else if (this.isVertical) {
101294 direction = (deltaY < 0) ? 'up' : 'down';
101295 distance = deltaY;
101296 }
101297
101298 this.direction = this.direction || direction;
101299
101300 // Invert the distance if we are going up or left so the distance is a positive number FROM the side
101301 if (this.direction == 'up') {
101302 distance = deltaY * -1;
101303 } else if (this.direction == 'left') {
101304 distance = deltaX * -1;
101305 }
101306
101307 this.distance = distance;
101308
101309 if (distance == 0) {
101310 return this.fail(this.self.DISTANCE_NOT_ENOUGH);
101311 }
101312
101313 if (!this.started) {
101314 // If this is the first move, check if we are close enough to the edge to begin
101315 if (this.direction == 'right' && this.startX > minDistance) {
101316 return this.fail(this.self.NOT_NEAR_EDGE);
101317 }
101318 else if (this.direction == 'down' && this.startY > minDistance) {
101319 return this.fail(this.self.NOT_NEAR_EDGE);
101320 }
101321 else if (this.direction == 'left' && (elementWidth - this.startX) > minDistance) {
101322 return this.fail(this.self.NOT_NEAR_EDGE);
101323 }
101324 else if (this.direction == 'up' && (elementHeight - this.startY) > minDistance) {
101325 return this.fail(this.self.NOT_NEAR_EDGE);
101326 }
101327
101328 // Start the event
101329 this.started = true;
101330 this.startTime = e.time;
101331
101332 this.fire('edgeswipestart', e, [touch], {
101333 touch: touch,
101334 direction: this.direction,
101335 distance: this.distance,
101336 duration: duration
101337 });
101338 } else {
101339 this.fire('edgeswipe', e, [touch], {
101340 touch: touch,
101341 direction: this.direction,
101342 distance: this.distance,
101343 duration: duration
101344 });
101345 }
101346 },
101347
101348 onTouchEnd: function(e) {
101349 if (this.onTouchMove(e) !== false) {
101350 var touch = e.changedTouches[0],
101351 duration = e.time - this.startTime;
101352
101353 this.fire('edgeswipeend', e, [touch], {
101354 touch: touch,
101355 direction: this.direction,
101356 distance: this.distance,
101357 duration: duration
101358 });
101359 }
101360 }
101361 });
101362
101363 /**
101364 * A event recognizer created to recognize horizontal swipe movements.
101365 *
101366 * @private
101367 */
101368 Ext.define('Ext.event.recognizer.HorizontalSwipe', {
101369 extend: Ext.event.recognizer.Swipe ,
101370
101371 handledEvents: ['swipe'],
101372
101373 onTouchStart: function(e) {
101374 if (this.callParent(arguments) === false) {
101375 return false;
101376 }
101377
101378 var touch = e.changedTouches[0];
101379
101380 this.startTime = e.time;
101381
101382 this.startX = touch.pageX;
101383 this.startY = touch.pageY;
101384 },
101385
101386 onTouchMove: function(e) {
101387 var touch = e.changedTouches[0],
101388 y = touch.pageY,
101389 absDeltaY = Math.abs(y - this.startY),
101390 time = e.time,
101391 maxDuration = this.getMaxDuration(),
101392 maxOffset = this.getMaxOffset();
101393
101394 if (time - this.startTime > maxDuration) {
101395 return this.fail(this.self.MAX_DURATION_EXCEEDED);
101396 }
101397
101398 if (absDeltaY > maxOffset) {
101399 return this.fail(this.self.MAX_OFFSET_EXCEEDED);
101400 }
101401 },
101402
101403 onTouchEnd: function(e) {
101404 if (this.onTouchMove(e) !== false) {
101405 var touch = e.changedTouches[0],
101406 x = touch.pageX,
101407 deltaX = x - this.startX,
101408 distance = Math.abs(deltaX),
101409 duration = e.time - this.startTime,
101410 minDistance = this.getMinDistance(),
101411 direction;
101412
101413 if (distance < minDistance) {
101414 return this.fail(this.self.DISTANCE_NOT_ENOUGH);
101415 }
101416
101417 direction = (deltaX < 0) ? 'left' : 'right';
101418
101419 this.fire('swipe', e, [touch], {
101420 touch: touch,
101421 direction: direction,
101422 distance: distance,
101423 duration: duration
101424 });
101425 }
101426 }
101427 });
101428
101429 /**
101430 * A event recognizer which knows when you tap and hold for more than 1 second.
101431 *
101432 * @private
101433 */
101434 Ext.define('Ext.event.recognizer.LongPress', {
101435 extend: Ext.event.recognizer.SingleTouch ,
101436
101437 inheritableStatics: {
101438 DURATION_NOT_ENOUGH: 0x20
101439 },
101440
101441 config: {
101442 minDuration: 1000
101443 },
101444
101445 handledEvents: ['longpress'],
101446
101447 /**
101448 * @member Ext.dom.Element
101449 * @event longpress
101450 * Fires when you touch and hold still for more than 1 second.
101451 * @param {Ext.event.Event} event The {@link Ext.event.Event} event encapsulating the DOM event.
101452 * @param {HTMLElement} node The target of the event.
101453 * @param {Object} options The options object passed to Ext.mixin.Observable.addListener.
101454 */
101455
101456 /**
101457 * @member Ext.dom.Element
101458 * @event taphold
101459 * @inheritdoc Ext.dom.Element#longpress
101460 */
101461
101462 fireLongPress: function(e) {
101463 var touch = e.changedTouches[0];
101464
101465 this.fire('longpress', e, [touch], {
101466 touch: touch,
101467 duration: this.getMinDuration()
101468 });
101469
101470 this.isLongPress = true;
101471 },
101472
101473 onTouchStart: function(e) {
101474 var me = this;
101475
101476 if (this.callParent(arguments) === false) {
101477 return false;
101478 }
101479
101480 this.isLongPress = false;
101481
101482 this.timer = setTimeout(function() {
101483 me.fireLongPress(e);
101484 }, this.getMinDuration());
101485 },
101486
101487 onTouchMove: function() {
101488 return this.fail(this.self.TOUCH_MOVED);
101489 },
101490
101491 onTouchEnd: function() {
101492 if (!this.isLongPress) {
101493 return this.fail(this.self.DURATION_NOT_ENOUGH);
101494 }
101495 },
101496
101497 fail: function() {
101498 clearTimeout(this.timer);
101499
101500 return this.callParent(arguments);
101501 }
101502
101503 }, function() {
101504 this.override({
101505 handledEvents: ['longpress', 'taphold'],
101506
101507 fire: function(eventName) {
101508 if (eventName === 'longpress') {
101509 var args = Array.prototype.slice.call(arguments);
101510 args[0] = 'taphold';
101511
101512 this.fire.apply(this, args);
101513 }
101514
101515 return this.callOverridden(arguments);
101516 }
101517 });
101518 });
101519
101520 /**
101521 * @private
101522 */
101523 Ext.define('Ext.event.recognizer.MultiTouch', {
101524 extend: Ext.event.recognizer.Touch ,
101525
101526 requiredTouchesCount: 2,
101527
101528 isTracking: false,
101529
101530 isStarted: false,
101531
101532 onTouchStart: function(e) {
101533 var requiredTouchesCount = this.requiredTouchesCount,
101534 touches = e.touches,
101535 touchesCount = touches.length;
101536
101537 if (touchesCount === requiredTouchesCount) {
101538 this.start(e);
101539 }
101540 else if (touchesCount > requiredTouchesCount) {
101541 this.end(e);
101542 }
101543 },
101544
101545 onTouchEnd: function(e) {
101546 this.end(e);
101547 },
101548
101549 start: function() {
101550 if (!this.isTracking) {
101551 this.isTracking = true;
101552 this.isStarted = false;
101553 }
101554 },
101555
101556 end: function(e) {
101557 if (this.isTracking) {
101558 this.isTracking = false;
101559
101560 if (this.isStarted) {
101561 this.isStarted = false;
101562
101563 this.fireEnd(e);
101564 }
101565 }
101566 }
101567 });
101568
101569 /**
101570 * A event recognizer which knows when you pinch.
101571 *
101572 * @private
101573 */
101574 Ext.define('Ext.event.recognizer.Pinch', {
101575 extend: Ext.event.recognizer.MultiTouch ,
101576
101577 requiredTouchesCount: 2,
101578
101579 handledEvents: ['pinchstart', 'pinch', 'pinchend'],
101580
101581 /**
101582 * @member Ext.dom.Element
101583 * @event pinchstart
101584 * Fired once when a pinch has started.
101585 * @param {Ext.event.Event} event The {@link Ext.event.Event} event encapsulating the DOM event.
101586 * @param {HTMLElement} node The target of the event.
101587 * @param {Object} options The options object passed to Ext.mixin.Observable.addListener.
101588 */
101589
101590 /**
101591 * @member Ext.dom.Element
101592 * @event pinch
101593 * Fires continuously when there is pinching (the touch must move for this to be fired).
101594 * @param {Ext.event.Event} event The {@link Ext.event.Event} event encapsulating the DOM event.
101595 * @param {HTMLElement} node The target of the event.
101596 * @param {Object} options The options object passed to Ext.mixin.Observable.addListener.
101597 */
101598
101599 /**
101600 * @member Ext.dom.Element
101601 * @event pinchend
101602 * Fires when a pinch has ended.
101603 * @param {Ext.event.Event} event The {@link Ext.event.Event} event encapsulating the DOM event.
101604 * @param {HTMLElement} node The target of the event.
101605 * @param {Object} options The options object passed to Ext.mixin.Observable.addListener.
101606 */
101607
101608 /**
101609 * @property {Number} scale
101610 * The scape of a pinch event.
101611 *
101612 * **This is only available when the event type is `pinch`**
101613 * @member Ext.event.Event
101614 */
101615
101616 startDistance: 0,
101617
101618 lastTouches: null,
101619
101620 onTouchMove: function(e) {
101621 if (!this.isTracking) {
101622 return;
101623 }
101624
101625 var touches = Array.prototype.slice.call(e.touches),
101626 firstPoint, secondPoint, distance;
101627
101628 firstPoint = touches[0].point;
101629 secondPoint = touches[1].point;
101630
101631 distance = firstPoint.getDistanceTo(secondPoint);
101632
101633 if (distance === 0) {
101634 return;
101635 }
101636
101637 if (!this.isStarted) {
101638
101639 this.isStarted = true;
101640
101641 this.startDistance = distance;
101642
101643 this.fire('pinchstart', e, touches, {
101644 touches: touches,
101645 distance: distance,
101646 scale: 1
101647 });
101648 }
101649 else {
101650 this.fire('pinch', e, touches, {
101651 touches: touches,
101652 distance: distance,
101653 scale: distance / this.startDistance
101654 });
101655 }
101656
101657 this.lastTouches = touches;
101658 },
101659
101660 fireEnd: function(e) {
101661 this.fire('pinchend', e, this.lastTouches);
101662 },
101663
101664 fail: function() {
101665 return this.callParent(arguments);
101666 }
101667 });
101668
101669 /**
101670 * A simple event recognizer which knows when you rotate.
101671 *
101672 * @private
101673 */
101674 Ext.define('Ext.event.recognizer.Rotate', {
101675 extend: Ext.event.recognizer.MultiTouch ,
101676
101677 requiredTouchesCount: 2,
101678
101679 handledEvents: ['rotatestart', 'rotate', 'rotateend'],
101680
101681 /**
101682 * @member Ext.dom.Element
101683 * @event rotatestart
101684 * Fired once when a rotation has started.
101685 * @param {Ext.event.Event} event The {@link Ext.event.Event} event encapsulating the DOM event.
101686 * @param {HTMLElement} node The target of the event.
101687 * @param {Object} options The options object passed to Ext.mixin.Observable.addListener.
101688 */
101689
101690 /**
101691 * @member Ext.dom.Element
101692 * @event rotate
101693 * Fires continuously when there is rotation (the touch must move for this to be fired).
101694 * When listening to this, ensure you know about the {@link Ext.event.Event#angle} and {@link Ext.event.Event#rotation}
101695 * properties in the `event` object.
101696 * @param {Ext.event.Event} event The {@link Ext.event.Event} event encapsulating the DOM event.
101697 * @param {HTMLElement} node The target of the event.
101698 * @param {Object} options The options object passed to Ext.mixin.Observable.addListener.
101699 */
101700
101701 /**
101702 * @member Ext.dom.Element
101703 * @event rotateend
101704 * Fires when a rotation event has ended.
101705 * @param {Ext.event.Event} event The {@link Ext.event.Event} event encapsulating the DOM event.
101706 * @param {HTMLElement} node The target of the event.
101707 * @param {Object} options The options object passed to Ext.mixin.Observable.addListener.
101708 */
101709
101710 /**
101711 * @property {Number} angle
101712 * The angle of the rotation.
101713 *
101714 * **This is only available when the event type is `rotate`**
101715 * @member Ext.event.Event
101716 */
101717
101718 /**
101719 * @property {Number} rotation
101720 * A amount of rotation, since the start of the event.
101721 *
101722 * **This is only available when the event type is `rotate`**
101723 * @member Ext.event.Event
101724 */
101725
101726 startAngle: 0,
101727
101728 lastTouches: null,
101729
101730 lastAngle: null,
101731
101732 onTouchMove: function(e) {
101733 if (!this.isTracking) {
101734 return;
101735 }
101736
101737 var touches = Array.prototype.slice.call(e.touches),
101738 lastAngle = this.lastAngle,
101739 firstPoint, secondPoint, angle, nextAngle, previousAngle, diff;
101740
101741 firstPoint = touches[0].point;
101742 secondPoint = touches[1].point;
101743
101744 angle = firstPoint.getAngleTo(secondPoint);
101745
101746 if (lastAngle !== null) {
101747 diff = Math.abs(lastAngle - angle);
101748 nextAngle = angle + 360;
101749 previousAngle = angle - 360;
101750
101751 if (Math.abs(nextAngle - lastAngle) < diff) {
101752 angle = nextAngle;
101753 }
101754 else if (Math.abs(previousAngle - lastAngle) < diff) {
101755 angle = previousAngle;
101756 }
101757 }
101758
101759 this.lastAngle = angle;
101760
101761 if (!this.isStarted) {
101762 this.isStarted = true;
101763
101764 this.startAngle = angle;
101765
101766 this.fire('rotatestart', e, touches, {
101767 touches: touches,
101768 angle: angle,
101769 rotation: 0
101770 });
101771 }
101772 else {
101773 this.fire('rotate', e, touches, {
101774 touches: touches,
101775 angle: angle,
101776 rotation: angle - this.startAngle
101777 });
101778 }
101779
101780 this.lastTouches = touches;
101781 },
101782
101783 fireEnd: function(e) {
101784 this.lastAngle = null;
101785 this.fire('rotateend', e, this.lastTouches);
101786 }
101787 });
101788
101789 /**
101790 * A simple event recogniser which knows when you tap.
101791 *
101792 * @private
101793 */
101794 Ext.define('Ext.event.recognizer.Tap', {
101795 extend: Ext.event.recognizer.SingleTouch ,
101796
101797 handledEvents: ['tap', 'tapcancel'],
101798
101799 config: {
101800 /**
101801 * @cfg {Number} moveDistance
101802 * The maximimum distance in pixels a touchstart event can travel and still be considered a tap event.
101803 */
101804
101805 moveDistance: 8
101806 },
101807
101808 onTouchStart: function(e) {
101809 if (this.callSuper(arguments) === false) {
101810 return false;
101811 }
101812
101813 this.startPoint = e.changedTouches[0].point;
101814 },
101815
101816 onTouchMove: function(e) {
101817 var touch = e.changedTouches[0],
101818 point = touch.point;
101819
101820 if (Math.abs(point.getDistanceTo(this.startPoint)) >= this.getMoveDistance()) {
101821 this.fire('tapcancel', e, [touch], {
101822 touch: touch
101823 });
101824 return this.fail(this.self.TOUCH_MOVED);
101825 }
101826 },
101827
101828 onTouchEnd: function(e) {
101829 var touch = e.changedTouches[0];
101830
101831 this.fire('tap', e, [touch], {
101832 touch: touch
101833 });
101834 }
101835 });
101836
101837 /**
101838 * A event recognizer created to recognize vertical swipe movements.
101839 *
101840 * This is disabled by default in Sencha Touch as it has a performance impact when your application
101841 * has vertical scrollers, plus, in most cases it is not very useful.
101842 *
101843 * If you wish to recognize vertical swipe movements in your application, please refer to the documentation of
101844 * {@link Ext.event.recognizer.Recognizer} and {@link Ext#setup}.
101845 *
101846 * @private
101847 */
101848 Ext.define('Ext.event.recognizer.VerticalSwipe', {
101849 extend: Ext.event.recognizer.Swipe ,
101850
101851 onTouchStart: function(e) {
101852 if (this.callParent(arguments) === false) {
101853 return false;
101854 }
101855
101856 var touch = e.changedTouches[0];
101857
101858 this.startTime = e.time;
101859
101860 this.startX = touch.pageX;
101861 this.startY = touch.pageY;
101862 },
101863
101864 onTouchMove: function(e) {
101865 var touch = e.changedTouches[0],
101866 x = touch.pageX,
101867 absDeltaX = Math.abs(x - this.startX),
101868 maxDuration = this.getMaxDuration(),
101869 maxOffset = this.getMaxOffset(),
101870 time = e.time;
101871
101872 if (time - this.startTime > maxDuration) {
101873 return this.fail(this.self.MAX_DURATION_EXCEEDED);
101874 }
101875
101876 if (absDeltaX > maxOffset) {
101877 return this.fail(this.self.MAX_OFFSET_EXCEEDED);
101878 }
101879 },
101880
101881 onTouchEnd: function(e) {
101882 if (this.onTouchMove(e) !== false) {
101883 var touch = e.changedTouches[0],
101884 y = touch.pageY,
101885 deltaY = y - this.startY,
101886 distance = Math.abs(deltaY),
101887 duration = e.time - this.startTime,
101888 minDistance = this.getMinDistance(),
101889 direction;
101890
101891 if (distance < minDistance) {
101892 return this.fail(this.self.DISTANCE_NOT_ENOUGH);
101893 }
101894
101895 direction = (deltaY < 0) ? 'up' : 'down';
101896
101897 this.fire('swipe', e, [touch], {
101898 touch : touch,
101899 distance : distance,
101900 duration : duration,
101901 direction : direction
101902 });
101903 }
101904 }
101905 });
101906
101907 /**
101908 * The checkbox field is an enhanced version of the native browser checkbox and is great for enabling your user to
101909 * choose one or more items from a set (for example choosing toppings for a pizza order). It works like any other
101910 * {@link Ext.field.Field field} and is usually found in the context of a form:
101911 *
101912 * ## Example
101913 *
101914 * @example miniphone preview
101915 * var form = Ext.create('Ext.form.Panel', {
101916 * fullscreen: true,
101917 * items: [
101918 * {
101919 * xtype: 'checkboxfield',
101920 * name : 'tomato',
101921 * label: 'Tomato',
101922 * value: 'tomato',
101923 * checked: true
101924 * },
101925 * {
101926 * xtype: 'checkboxfield',
101927 * name : 'salami',
101928 * label: 'Salami'
101929 * },
101930 * {
101931 * xtype: 'toolbar',
101932 * docked: 'bottom',
101933 * items: [
101934 * { xtype: 'spacer' },
101935 * {
101936 * text: 'getValues',
101937 * handler: function() {
101938 * var form = Ext.ComponentQuery.query('formpanel')[0],
101939 * values = form.getValues();
101940 *
101941 * Ext.Msg.alert(null,
101942 * "Tomato: " + ((values.tomato) ? "yes" : "no") +
101943 * "<br />Salami: " + ((values.salami) ? "yes" : "no")
101944 * );
101945 * }
101946 * },
101947 * { xtype: 'spacer' }
101948 * ]
101949 * }
101950 * ]
101951 * });
101952 *
101953 *
101954 * The form above contains two check boxes - one for Tomato, one for Salami. We configured the Tomato checkbox to be
101955 * checked immediately on load, and the Salami checkbox to be unchecked. We also specified an optional text
101956 * {@link #value} that will be sent when we submit the form. We can get this value using the Form's
101957 * {@link Ext.form.Panel#getValues getValues} function, or have it sent as part of the data that is sent when the
101958 * form is submitted:
101959 *
101960 * form.getValues(); //contains a key called 'tomato' if the Tomato field is still checked
101961 * form.submit(); //will send 'tomato' in the form submission data
101962 *
101963 * For more information regarding forms and fields, please review [Using Forms in Sencha Touch Guide](../../../components/forms.html)
101964 */
101965 Ext.define('Ext.field.Checkbox', {
101966 extend: Ext.field.Field ,
101967 alternateClassName: 'Ext.form.Checkbox',
101968
101969 xtype: 'checkboxfield',
101970 qsaLeftRe: /[\[]/g,
101971 qsaRightRe: /[\]]/g,
101972
101973 isCheckbox: true,
101974
101975 /**
101976 * @event change
101977 * Fires just before the field blurs if the field value has changed.
101978 * @param {Ext.field.Checkbox} this This field.
101979 * @param {Boolean} newValue The new value.
101980 * @param {Boolean} oldValue The original value.
101981 */
101982
101983 /**
101984 * @event check
101985 * Fires when the checkbox is checked.
101986 * @param {Ext.field.Checkbox} this This checkbox.
101987 * @param {Ext.EventObject} e This event object.
101988 */
101989
101990 /**
101991 * @event uncheck
101992 * Fires when the checkbox is unchecked.
101993 * @param {Ext.field.Checkbox} this This checkbox.
101994 * @param {Ext.EventObject} e This event object.
101995 */
101996
101997 config: {
101998 /**
101999 * @cfg
102000 * @inheritdoc
102001 */
102002 ui: 'checkbox',
102003
102004 /**
102005 * @cfg {String} value The string value to submit if the item is in a checked state.
102006 * @accessor
102007 */
102008 value: '',
102009
102010 /**
102011 * @cfg {Boolean} checked `true` if the checkbox should render initially checked.
102012 * @accessor
102013 */
102014 checked: false,
102015
102016 /**
102017 * @cfg {Number} tabIndex
102018 * @hide
102019 */
102020 tabIndex: -1,
102021
102022 /**
102023 * @cfg
102024 * @inheritdoc
102025 */
102026 component: {
102027 xtype: 'input',
102028 type: 'checkbox',
102029 useMask: true,
102030 cls: Ext.baseCSSPrefix + 'input-checkbox'
102031 }
102032
102033 /**
102034 * @cfg {Boolean} labelMaskTap
102035 * @private
102036 */
102037 },
102038
102039 platformConfig: [{
102040 theme: ['Windows', 'Blackberry', 'Blackberry103', 'Tizen'],
102041 labelAlign: 'left'
102042 }],
102043
102044 // @private
102045 initialize: function() {
102046 var me = this,
102047 component = me.getComponent();
102048
102049 me.callParent();
102050
102051 component.on({
102052 scope: me,
102053 order: 'before',
102054 masktap: 'onMaskTap'
102055 });
102056
102057 component.doMaskTap = Ext.emptyFn;
102058
102059 me.label.on({
102060 scope: me,
102061 tap: 'onMaskTap'
102062 });
102063 },
102064
102065 // @private
102066 doInitValue: function() {
102067 var me = this,
102068 initialConfig = me.getInitialConfig();
102069
102070 // you can have a value or checked config, but checked get priority
102071 if (initialConfig.hasOwnProperty('value')) {
102072 me.originalState = initialConfig.value;
102073 }
102074
102075 if (initialConfig.hasOwnProperty('checked')) {
102076 me.originalState = initialConfig.checked;
102077 }
102078
102079 me.callParent(arguments);
102080 },
102081
102082 // @private
102083 updateInputType: function(newInputType) {
102084 var component = this.getComponent();
102085 if (component) {
102086 component.setType(newInputType);
102087 }
102088 },
102089
102090 // @private
102091 updateName: function(newName) {
102092 var component = this.getComponent();
102093 if (component) {
102094 component.setName(newName);
102095 }
102096 },
102097
102098 /**
102099 * Returns the field checked value.
102100 * @return {Mixed} The field value.
102101 */
102102 getChecked: function() {
102103 // we need to get the latest value from the {@link #input} and then update the value
102104 this._checked = this.getComponent().getChecked();
102105 return this._checked;
102106 },
102107
102108 /**
102109 * Returns the submit value for the checkbox which can be used when submitting forms.
102110 * @return {Boolean/String} value The value of {@link #value} or `true`, if {@link #checked}.
102111 */
102112 getSubmitValue: function() {
102113 return (this.getChecked()) ? Ext.isEmpty(this._value) ? true : this._value : null;
102114 },
102115
102116 setChecked: function(newChecked) {
102117 this.updateChecked(newChecked);
102118 this._checked = newChecked;
102119 },
102120
102121 updateChecked: function(newChecked) {
102122 this.getComponent().setChecked(newChecked);
102123
102124 // only call onChange (which fires events) if the component has been initialized
102125 if (this.initialized) {
102126 this.onChange();
102127 }
102128 },
102129
102130 // @private
102131 onMaskTap: function(component, e) {
102132 var me = this,
102133 dom = me.getComponent().input.dom;
102134
102135 if (me.getDisabled()) {
102136 return false;
102137 }
102138
102139 //we must manually update the input dom with the new checked value
102140 dom.checked = !dom.checked;
102141
102142 me.onChange(e);
102143
102144 //return false so the mask does not disappear
102145 return false;
102146 },
102147
102148 /**
102149 * Fires the `check` or `uncheck` event when the checked value of this component changes.
102150 * @private
102151 */
102152 onChange: function(e) {
102153 var me = this,
102154 oldChecked = me._checked,
102155 newChecked = me.getChecked();
102156
102157 // only fire the event when the value changes
102158 if (oldChecked != newChecked) {
102159 if (newChecked) {
102160 me.fireEvent('check', me, e);
102161 } else {
102162 me.fireEvent('uncheck', me, e);
102163 }
102164
102165 me.fireEvent('change', me, newChecked, oldChecked);
102166 }
102167 },
102168
102169 /**
102170 * @method
102171 * Method called when this {@link Ext.field.Checkbox} has been checked.
102172 */
102173 doChecked: Ext.emptyFn,
102174
102175 /**
102176 * @method
102177 * Method called when this {@link Ext.field.Checkbox} has been unchecked.
102178 */
102179 doUnChecked: Ext.emptyFn,
102180
102181 /**
102182 * Returns the checked state of the checkbox.
102183 * @return {Boolean} `true` if checked, `false` otherwise.
102184 */
102185 isChecked: function() {
102186 return this.getChecked();
102187 },
102188
102189 /**
102190 * Set the checked state of the checkbox to `true`.
102191 * @return {Ext.field.Checkbox} This checkbox.
102192 */
102193 check: function() {
102194 return this.setChecked(true);
102195 },
102196
102197 /**
102198 * Set the checked state of the checkbox to `false`.
102199 * @return {Ext.field.Checkbox} This checkbox.
102200 */
102201 uncheck: function() {
102202 return this.setChecked(false);
102203 },
102204
102205 getSameGroupFields: function() {
102206 var me = this,
102207 component = me.up('formpanel') || me.up('fieldset'),
102208 name = me.getName(),
102209 replaceLeft = me.qsaLeftRe,
102210 replaceRight = me.qsaRightRe,
102211 //handle baseCls with multiple class values
102212 baseCls = me.getBaseCls().split(' ').join('.'),
102213 components = [],
102214 elements, element, i, ln;
102215
102216 if (!component) {
102217 // <debug>
102218 Ext.Logger.warn('Ext.field.Radio components must always be descendants of an Ext.form.Panel or Ext.form.FieldSet.');
102219 // </debug>
102220 component = Ext.Viewport;
102221 }
102222
102223 // This is to handle ComponentQuery's lack of handling [name=foo[bar]] properly
102224 name = name.replace(replaceLeft, '\\[');
102225 name = name.replace(replaceRight, '\\]');
102226
102227 elements = Ext.query('[name=' + name + ']', component.element.dom);
102228 ln = elements.length;
102229 for (i = 0; i < ln; i++) {
102230 element = elements[i];
102231 element = Ext.fly(element).up('.' + baseCls);
102232 if (element && element.id) {
102233 components.push(Ext.getCmp(element.id));
102234 }
102235 }
102236 return components;
102237 },
102238
102239 /**
102240 * Returns an array of values from the checkboxes in the group that are checked.
102241 * @return {Array}
102242 */
102243 getGroupValues: function() {
102244 var values = [];
102245
102246 this.getSameGroupFields().forEach(function(field) {
102247 if (field.getChecked()) {
102248 values.push(field.getValue());
102249 }
102250 });
102251
102252 return values;
102253 },
102254
102255 /**
102256 * Set the status of all matched checkboxes in the same group to checked.
102257 * @param {Array} values An array of values.
102258 * @return {Ext.field.Checkbox} This checkbox.
102259 */
102260 setGroupValues: function(values) {
102261 this.getSameGroupFields().forEach(function(field) {
102262 field.setChecked((values.indexOf(field.getValue()) !== -1));
102263 });
102264
102265 return this;
102266 },
102267
102268 /**
102269 * Resets the status of all matched checkboxes in the same group to checked.
102270 * @return {Ext.field.Checkbox} This checkbox.
102271 */
102272 resetGroupValues: function() {
102273 this.getSameGroupFields().forEach(function(field) {
102274 field.setChecked(field.originalState);
102275 });
102276
102277 return this;
102278 },
102279
102280 reset: function() {
102281 this.setChecked(this.originalState);
102282 return this;
102283 }
102284 });
102285
102286 /**
102287 * @private
102288 *
102289 * A general {@link Ext.picker.Picker} slot class. Slots are used to organize multiple scrollable slots into
102290 * a single {@link Ext.picker.Picker}.
102291 *
102292 * {
102293 * name : 'limit_speed',
102294 * title: 'Speed Limit',
102295 * data : [
102296 * {text: '50 KB/s', value: 50},
102297 * {text: '100 KB/s', value: 100},
102298 * {text: '200 KB/s', value: 200},
102299 * {text: '300 KB/s', value: 300}
102300 * ]
102301 * }
102302 *
102303 * See the {@link Ext.picker.Picker} documentation on how to use slots.
102304 */
102305 Ext.define('Ext.picker.Slot', {
102306 extend: Ext.dataview.DataView ,
102307 xtype : 'pickerslot',
102308 alternateClassName: 'Ext.Picker.Slot',
102309
102310
102311
102312
102313
102314
102315
102316 /**
102317 * @event slotpick
102318 * Fires whenever an slot is picked
102319 * @param {Ext.picker.Slot} this
102320 * @param {Mixed} value The value of the pick
102321 * @param {HTMLElement} node The node element of the pick
102322 */
102323
102324 isSlot: true,
102325
102326 config: {
102327 /**
102328 * @cfg {String} title The title to use for this slot, or `null` for no title.
102329 * @accessor
102330 */
102331 title: null,
102332
102333 /**
102334 * @private
102335 * @cfg {Boolean} showTitle
102336 * @accessor
102337 */
102338 showTitle: true,
102339
102340 /**
102341 * @private
102342 * @cfg {String} cls The main component class
102343 * @accessor
102344 */
102345 cls: Ext.baseCSSPrefix + 'picker-slot',
102346
102347 /**
102348 * @cfg {String} name (required) The name of this slot.
102349 * @accessor
102350 */
102351 name: null,
102352
102353 /**
102354 * @cfg {Number} value The value of this slot
102355 * @accessor
102356 */
102357 value: null,
102358
102359 /**
102360 * @cfg {Number} flex
102361 * @accessor
102362 * @private
102363 */
102364 flex: 1,
102365
102366 /**
102367 * @cfg {String} align The horizontal alignment of the slot's contents.
102368 *
102369 * Valid values are: "left", "center", and "right".
102370 * @accessor
102371 */
102372 align: 'left',
102373
102374 /**
102375 * @cfg {String} displayField The display field in the store.
102376 * @accessor
102377 */
102378 displayField: 'text',
102379
102380 /**
102381 * @cfg {String} valueField The value field in the store.
102382 * @accessor
102383 */
102384 valueField: 'value',
102385
102386 /**
102387 * @cfg {String} itemTpl The template to be used in this slot.
102388 * If you set this, {@link #displayField} will be ignored.
102389 */
102390 itemTpl: null,
102391
102392 /**
102393 * @cfg {Object} scrollable
102394 * @accessor
102395 * @hide
102396 */
102397 scrollable: {
102398 direction: 'vertical',
102399 indicators: false,
102400 momentumEasing: {
102401 minVelocity: 2
102402 },
102403 slotSnapEasing: {
102404 duration: 100
102405 }
102406 },
102407
102408 /**
102409 * @cfg {Boolean} verticallyCenterItems
102410 * @private
102411 */
102412 verticallyCenterItems: true
102413 },
102414
102415 platformConfig: [{
102416 theme: ['Windows'],
102417 title: 'choose an item'
102418 // verticallyCenterItems: false
102419 }],
102420
102421 constructor: function() {
102422 /**
102423 * @property selectedIndex
102424 * @type Number
102425 * The current `selectedIndex` of the picker slot.
102426 * @private
102427 */
102428 this.selectedIndex = 0;
102429
102430 /**
102431 * @property picker
102432 * @type Ext.picker.Picker
102433 * A reference to the owner Picker.
102434 * @private
102435 */
102436
102437 this.callParent(arguments);
102438 },
102439
102440 /**
102441 * Sets the title for this dataview by creating element.
102442 * @param {String} title
102443 * @return {String}
102444 */
102445 applyTitle: function(title) {
102446 //check if the title isnt defined
102447 if (title) {
102448 //create a new title element
102449 title = Ext.create('Ext.Component', {
102450 cls: Ext.baseCSSPrefix + 'picker-slot-title',
102451 docked: 'top',
102452 html: title
102453 });
102454 }
102455
102456 return title;
102457 },
102458
102459 updateTitle: function(newTitle, oldTitle) {
102460 if (newTitle) {
102461 this.add(newTitle);
102462 this.setupBar();
102463 }
102464
102465 if (oldTitle) {
102466 this.remove(oldTitle);
102467 }
102468 },
102469
102470 updateShowTitle: function(showTitle) {
102471 var title = this.getTitle(),
102472 mode = showTitle ? 'show' : 'hide';
102473 if (title) {
102474 title.on(mode, this.setupBar, this, { single: true, delay: 50 });
102475 title[showTitle ? 'show' : 'hide']();
102476 }
102477 },
102478
102479 updateDisplayField: function(newDisplayField) {
102480 if (!this.config.itemTpl) {
102481 this.setItemTpl('<div class="' + Ext.baseCSSPrefix + 'picker-item {cls} <tpl if="extra">' + Ext.baseCSSPrefix + 'picker-invalid</tpl>">{' + newDisplayField + '}</div>');
102482 }
102483 },
102484
102485 /**
102486 * Updates the {@link #align} configuration
102487 */
102488 updateAlign: function(newAlign, oldAlign) {
102489 var element = this.element;
102490 element.addCls(Ext.baseCSSPrefix + 'picker-' + newAlign);
102491 element.removeCls(Ext.baseCSSPrefix + 'picker-' + oldAlign);
102492 },
102493
102494 /**
102495 * Looks at the {@link #data} configuration and turns it into {@link #store}.
102496 * @param {Object} data
102497 * @return {Object}
102498 */
102499 applyData: function(data) {
102500 var parsedData = [],
102501 ln = data && data.length,
102502 i, item, obj;
102503
102504 if (data && Ext.isArray(data) && ln) {
102505 for (i = 0; i < ln; i++) {
102506 item = data[i];
102507 obj = {};
102508 if (Ext.isArray(item)) {
102509 obj[this.valueField] = item[0];
102510 obj[this.displayField] = item[1];
102511 }
102512 else if (Ext.isString(item)) {
102513 obj[this.valueField] = item;
102514 obj[this.displayField] = item;
102515 }
102516 else if (Ext.isObject(item)) {
102517 obj = item;
102518 }
102519 parsedData.push(obj);
102520 }
102521 }
102522
102523 return data;
102524 },
102525
102526 // @private
102527 initialize: function() {
102528 this.callParent();
102529
102530 var scroller = this.getScrollable().getScroller();
102531
102532 this.on({
102533 scope: this,
102534 painted: 'onPainted',
102535 itemtap: 'doItemTap'
102536 });
102537
102538 this.element.on({
102539 scope: this,
102540 touchstart: 'onTouchStart',
102541 touchend: 'onTouchEnd'
102542 });
102543
102544 scroller.on({
102545 scope: this,
102546 scrollend: 'onScrollEnd'
102547 });
102548 },
102549
102550 // @private
102551 onPainted: function() {
102552 this.setupBar();
102553 },
102554
102555 /**
102556 * Returns an instance of the owner picker.
102557 * @return {Object}
102558 * @private
102559 */
102560 getPicker: function() {
102561 if (!this.picker) {
102562 this.picker = this.getParent();
102563 }
102564
102565 return this.picker;
102566 },
102567
102568 // @private
102569 setupBar: function() {
102570 if (!this.rendered) {
102571 //if the component isnt rendered yet, there is no point in calculating the padding just eyt
102572 return;
102573 }
102574
102575 var element = this.element,
102576 innerElement = this.innerElement,
102577 picker = this.getPicker(),
102578 bar = picker.bar,
102579 value = this.getValue(),
102580 showTitle = this.getShowTitle(),
102581 title = this.getTitle(),
102582 scrollable = this.getScrollable(),
102583 scroller = scrollable.getScroller(),
102584 titleHeight = 0,
102585 barHeight, padding;
102586
102587 barHeight = bar.dom.getBoundingClientRect().height;
102588
102589 if (showTitle && title) {
102590 titleHeight = title.element.getHeight();
102591 }
102592
102593 padding = Math.ceil((element.getHeight() - titleHeight - barHeight) / 2);
102594
102595 if (this.getVerticallyCenterItems()) {
102596 innerElement.setStyle({
102597 padding: padding + 'px 0 ' + padding + 'px'
102598 });
102599 }
102600
102601 scroller.refresh();
102602 scroller.setSlotSnapSize(barHeight);
102603 this.setValue(value);
102604 },
102605
102606 // @private
102607 doItemTap: function(list, index, item, e) {
102608 var me = this;
102609 me.selectedIndex = index;
102610 me.selectedNode = item;
102611 me.scrollToItem(item, true);
102612 },
102613
102614 // @private
102615 scrollToItem: function(item, animated) {
102616 var y = item.getY(),
102617 parentEl = item.parent(),
102618 parentY = parentEl.getY(),
102619 scrollView = this.getScrollable(),
102620 scroller = scrollView.getScroller(),
102621 difference;
102622
102623 difference = y - parentY;
102624
102625 scroller.scrollTo(0, difference, animated);
102626 },
102627
102628 // @private
102629 onTouchStart: function() {
102630 this.element.addCls(Ext.baseCSSPrefix + 'scrolling');
102631 },
102632
102633 // @private
102634 onTouchEnd: function() {
102635 this.element.removeCls(Ext.baseCSSPrefix + 'scrolling');
102636 },
102637
102638 // @private
102639 onScrollEnd: function(scroller, x, y) {
102640 var me = this,
102641 index = Math.round(y / me.picker.bar.dom.getBoundingClientRect().height),
102642 viewItems = me.getViewItems(),
102643 item = viewItems[index];
102644
102645 if (item) {
102646 me.selectedIndex = index;
102647 me.selectedNode = item;
102648
102649 me.fireEvent('slotpick', me, me.getValue(), me.selectedNode);
102650 }
102651 },
102652
102653 /**
102654 * Returns the value of this slot
102655 * @private
102656 */
102657 getValue: function(useDom) {
102658 var store = this.getStore(),
102659 record, value;
102660
102661 if (!store) {
102662 return;
102663 }
102664
102665 if (!this.rendered || !useDom) {
102666 return this._value;
102667 }
102668
102669 //if the value is ever false, that means we do not want to return anything
102670 if (this._value === false) {
102671 return null;
102672 }
102673
102674 record = store.getAt(this.selectedIndex);
102675
102676 value = record ? record.get(this.getValueField()) : null;
102677
102678 return value;
102679 },
102680
102681 /**
102682 * Sets the value of this slot
102683 * @private
102684 */
102685 setValue: function(value) {
102686 return this.doSetValue(value);
102687 },
102688
102689 /**
102690 * Sets the value of this slot
102691 * @private
102692 */
102693 setValueAnimated: function(value) {
102694 return this.doSetValue(value, true);
102695 },
102696
102697 doSetValue: function(value, animated) {
102698 if (!this.rendered) {
102699 //we don't want to call this until the slot has been rendered
102700 this._value = value;
102701 return;
102702 }
102703
102704 var store = this.getStore(),
102705 viewItems = this.getViewItems(),
102706 valueField = this.getValueField(),
102707 index, item;
102708
102709 index = store.findExact(valueField, value);
102710
102711 if (index == -1) {
102712 index = 0;
102713 }
102714
102715 item = Ext.get(viewItems[index]);
102716
102717 this.selectedIndex = index;
102718 if (item) {
102719 this.scrollToItem(item, (animated) ? {
102720 duration: 100
102721 } : false);
102722 this.select(this.selectedIndex);
102723 }
102724
102725 this._value = value;
102726 }
102727 });
102728
102729 /**
102730 * A general picker class. {@link Ext.picker.Slot}s are used to organize multiple scrollable slots into a single picker. {@link #slots} is
102731 * the only necessary configuration.
102732 *
102733 * The {@link #slots} configuration with a few key values:
102734 *
102735 * - `name`: The name of the slot (will be the key when using {@link #getValues} in this {@link Ext.picker.Picker}).
102736 * - `title`: The title of this slot (if {@link #useTitles} is set to `true`).
102737 * - `data`/`store`: The data or store to use for this slot.
102738 *
102739 * Remember, {@link Ext.picker.Slot} class extends from {@link Ext.dataview.DataView}.
102740 *
102741 * ## Examples
102742 *
102743 * @example miniphone preview
102744 * var picker = Ext.create('Ext.Picker', {
102745 * slots: [
102746 * {
102747 * name : 'limit_speed',
102748 * title: 'Speed',
102749 * data : [
102750 * {text: '50 KB/s', value: 50},
102751 * {text: '100 KB/s', value: 100},
102752 * {text: '200 KB/s', value: 200},
102753 * {text: '300 KB/s', value: 300}
102754 * ]
102755 * }
102756 * ]
102757 * });
102758 * Ext.Viewport.add(picker);
102759 * picker.show();
102760 *
102761 * You can also customize the top toolbar on the {@link Ext.picker.Picker} by changing the {@link #doneButton} and {@link #cancelButton} configurations:
102762 *
102763 * @example miniphone preview
102764 * var picker = Ext.create('Ext.Picker', {
102765 * doneButton: 'I\'m done!',
102766 * cancelButton: false,
102767 * slots: [
102768 * {
102769 * name : 'limit_speed',
102770 * title: 'Speed',
102771 * data : [
102772 * {text: '50 KB/s', value: 50},
102773 * {text: '100 KB/s', value: 100},
102774 * {text: '200 KB/s', value: 200},
102775 * {text: '300 KB/s', value: 300}
102776 * ]
102777 * }
102778 * ]
102779 * });
102780 * Ext.Viewport.add(picker);
102781 * picker.show();
102782 *
102783 * Or by passing a custom {@link #toolbar} configuration:
102784 *
102785 * @example miniphone preview
102786 * var picker = Ext.create('Ext.Picker', {
102787 * doneButton: false,
102788 * cancelButton: false,
102789 * toolbar: {
102790 * ui: 'light',
102791 * title: 'My Picker!'
102792 * },
102793 * slots: [
102794 * {
102795 * name : 'limit_speed',
102796 * title: 'Speed',
102797 * data : [
102798 * {text: '50 KB/s', value: 50},
102799 * {text: '100 KB/s', value: 100},
102800 * {text: '200 KB/s', value: 200},
102801 * {text: '300 KB/s', value: 300}
102802 * ]
102803 * }
102804 * ]
102805 * });
102806 * Ext.Viewport.add(picker);
102807 * picker.show();
102808 */
102809 Ext.define('Ext.picker.Picker', {
102810 extend: Ext.Sheet ,
102811 alias : 'widget.picker',
102812 alternateClassName: 'Ext.Picker',
102813
102814
102815 isPicker: true,
102816
102817 /**
102818 * @event pick
102819 * Fired when a slot has been picked
102820 * @param {Ext.Picker} this This Picker.
102821 * @param {Object} The values of this picker's slots, in `{name:'value'}` format.
102822 * @param {Ext.Picker.Slot} slot An instance of Ext.Picker.Slot that has been picked.
102823 */
102824
102825 /**
102826 * @event change
102827 * Fired when the value of this picker has changed the Done button has been pressed.
102828 * @param {Ext.picker.Picker} this This Picker.
102829 * @param {Object} value The values of this picker's slots, in `{name:'value'}` format.
102830 */
102831
102832 /**
102833 * @event cancel
102834 * Fired when the cancel button is tapped and the values are reverted back to
102835 * what they were.
102836 * @param {Ext.Picker} this This Picker.
102837 */
102838
102839 config: {
102840 /**
102841 * @cfg
102842 * @inheritdoc
102843 */
102844 baseCls: Ext.baseCSSPrefix + 'picker',
102845
102846 /**
102847 * @cfg {String/Mixed} doneButton
102848 * Can be either:
102849 *
102850 * - A {String} text to be used on the Done button.
102851 * - An {Object} as config for {@link Ext.Button}.
102852 * - `false` or `null` to hide it.
102853 * @accessor
102854 */
102855 doneButton: true,
102856
102857 /**
102858 * @cfg {String/Mixed} cancelButton
102859 * Can be either:
102860 *
102861 * - A {String} text to be used on the Cancel button.
102862 * - An {Object} as config for {@link Ext.Button}.
102863 * - `false` or `null` to hide it.
102864 * @accessor
102865 */
102866 cancelButton: true,
102867
102868 /**
102869 * @cfg {Boolean} useTitles
102870 * Generate a title header for each individual slot and use
102871 * the title configuration of the slot.
102872 * @accessor
102873 */
102874 useTitles: false,
102875
102876 /**
102877 * @cfg {Array} slots
102878 * An array of slot configurations.
102879 *
102880 * - `name` {String} - Name of the slot
102881 * - `data` {Array} - An array of text/value pairs in the format `{text: 'myKey', value: 'myValue'}`
102882 * - `title` {String} - Title of the slot. This is used in conjunction with `useTitles: true`.
102883 *
102884 * @accessor
102885 */
102886 slots: null,
102887
102888 /**
102889 * @cfg {String/Number} value The value to initialize the picker with.
102890 * @accessor
102891 */
102892 value: null,
102893
102894 /**
102895 * @cfg {Number} height
102896 * The height of the picker.
102897 * @accessor
102898 */
102899 height: 220,
102900
102901 /**
102902 * @cfg
102903 * @inheritdoc
102904 */
102905 layout: {
102906 type : 'hbox',
102907 align: 'stretch'
102908 },
102909
102910 /**
102911 * @cfg
102912 * @hide
102913 */
102914 centered: false,
102915
102916 /**
102917 * @cfg
102918 * @inheritdoc
102919 */
102920 left : 0,
102921
102922 /**
102923 * @cfg
102924 * @inheritdoc
102925 */
102926 right: 0,
102927
102928 /**
102929 * @cfg
102930 * @inheritdoc
102931 */
102932 bottom: 0,
102933
102934 // @private
102935 defaultType: 'pickerslot',
102936
102937 toolbarPosition: 'top',
102938
102939 /**
102940 * @cfg {Ext.TitleBar/Ext.Toolbar/Object} toolbar
102941 * The toolbar which contains the {@link #doneButton} and {@link #cancelButton} buttons.
102942 * You can override this if you wish, and add your own configurations. Just ensure that you take into account
102943 * the {@link #doneButton} and {@link #cancelButton} configurations.
102944 *
102945 * The default xtype is a {@link Ext.TitleBar}:
102946 *
102947 * toolbar: {
102948 * items: [
102949 * {
102950 * xtype: 'button',
102951 * text: 'Left',
102952 * align: 'left'
102953 * },
102954 * {
102955 * xtype: 'button',
102956 * text: 'Right',
102957 * align: 'left'
102958 * }
102959 * ]
102960 * }
102961 *
102962 * Or to use a {@link Ext.Toolbar instead}:
102963 *
102964 * toolbar: {
102965 * xtype: 'toolbar',
102966 * items: [
102967 * {
102968 * xtype: 'button',
102969 * text: 'Left'
102970 * },
102971 * {
102972 * xtype: 'button',
102973 * text: 'Left Two'
102974 * }
102975 * ]
102976 * }
102977 *
102978 * @accessor
102979 */
102980 toolbar: {
102981 xtype: 'titlebar'
102982 }
102983 },
102984
102985 platformConfig: [{
102986 theme: ['Windows'],
102987 height: '100%',
102988 toolbarPosition: 'bottom',
102989 toolbar: {
102990 xtype: 'toolbar',
102991 layout: {
102992 type: 'hbox',
102993 pack: 'center'
102994 }
102995 },
102996 doneButton: {
102997 iconCls: 'check2',
102998 ui: 'round',
102999 text: ''
103000 },
103001 cancelButton: {
103002 iconCls: 'delete',
103003 ui: 'round',
103004 text: ''
103005 }
103006 }, {
103007 theme: ['CupertinoClassic'],
103008 toolbar: {
103009 ui: 'black'
103010 }
103011 }, {
103012 theme: ['MountainView'],
103013 toolbarPosition: 'bottom',
103014 toolbar: {
103015 defaults: {
103016 flex: 1
103017 }
103018 }
103019 }],
103020
103021 initialize: function() {
103022 var me = this,
103023 clsPrefix = Ext.baseCSSPrefix,
103024 innerElement = this.innerElement;
103025
103026 //insert the mask, and the picker bar
103027 this.mask = innerElement.createChild({
103028 cls: clsPrefix + 'picker-mask'
103029 });
103030
103031 this.bar = this.mask.createChild({
103032 cls: clsPrefix + 'picker-bar'
103033 });
103034
103035 me.on({
103036 scope : this,
103037 delegate: 'pickerslot',
103038 slotpick: 'onSlotPick'
103039 });
103040 },
103041
103042 /**
103043 * @private
103044 */
103045 applyToolbar: function(config) {
103046 if (config === true) {
103047 config = {};
103048 }
103049
103050 Ext.applyIf(config, {
103051 docked: this.getToolbarPosition()
103052 });
103053
103054 return Ext.factory(config, 'Ext.TitleBar', this.getToolbar());
103055 },
103056
103057 /**
103058 * @private
103059 */
103060 updateToolbar: function(newToolbar, oldToolbar) {
103061 if (newToolbar) {
103062 this.add(newToolbar);
103063 }
103064
103065 if (oldToolbar) {
103066 this.remove(oldToolbar);
103067 }
103068 },
103069
103070 /**
103071 * Updates the {@link #doneButton} configuration. Will change it into a button when appropriate, or just update the text if needed.
103072 * @param {Object} config
103073 * @return {Object}
103074 */
103075 applyDoneButton: function(config) {
103076 if (config) {
103077 if (Ext.isBoolean(config)) {
103078 config = {};
103079 }
103080
103081 if (typeof config == "string") {
103082 config = {
103083 text: config
103084 };
103085 }
103086
103087 Ext.applyIf(config, {
103088 ui: 'action',
103089 align: 'right',
103090 text: 'Done'
103091 });
103092 }
103093
103094 return Ext.factory(config, 'Ext.Button', this.getDoneButton());
103095 },
103096
103097 updateDoneButton: function(newDoneButton, oldDoneButton) {
103098 var toolbar = this.getToolbar();
103099
103100 if (newDoneButton) {
103101 toolbar.add(newDoneButton);
103102 newDoneButton.on('tap', this.onDoneButtonTap, this);
103103 } else if (oldDoneButton) {
103104 toolbar.remove(oldDoneButton);
103105 }
103106 },
103107
103108 /**
103109 * Updates the {@link #cancelButton} configuration. Will change it into a button when appropriate, or just update the text if needed.
103110 * @param {Object} config
103111 * @return {Object}
103112 */
103113 applyCancelButton: function(config) {
103114 if (config) {
103115 if (Ext.isBoolean(config)) {
103116 config = {};
103117 }
103118
103119 if (typeof config == "string") {
103120 config = {
103121 text: config
103122 };
103123 }
103124
103125 Ext.applyIf(config, {
103126 align: 'left',
103127 text: 'Cancel'
103128 });
103129 }
103130
103131 return Ext.factory(config, 'Ext.Button', this.getCancelButton());
103132 },
103133
103134 updateCancelButton: function(newCancelButton, oldCancelButton) {
103135 var toolbar = this.getToolbar();
103136
103137 if (newCancelButton) {
103138 toolbar.add(newCancelButton);
103139 newCancelButton.on('tap', this.onCancelButtonTap, this);
103140 } else if (oldCancelButton) {
103141 toolbar.remove(oldCancelButton);
103142 }
103143 },
103144
103145 /**
103146 * @private
103147 */
103148 updateUseTitles: function(useTitles) {
103149 var innerItems = this.getInnerItems(),
103150 ln = innerItems.length,
103151 cls = Ext.baseCSSPrefix + 'use-titles',
103152 i, innerItem;
103153
103154 //add a cls onto the picker
103155 if (useTitles) {
103156 this.addCls(cls);
103157 } else {
103158 this.removeCls(cls);
103159 }
103160
103161 //show the time on each of the slots
103162 for (i = 0; i < ln; i++) {
103163 innerItem = innerItems[i];
103164
103165 if (innerItem.isSlot) {
103166 innerItem.setShowTitle(useTitles);
103167 }
103168 }
103169 },
103170
103171 applySlots: function(slots) {
103172 //loop through each of the slots and add a reference to this picker
103173 if (slots) {
103174 var ln = slots.length,
103175 i;
103176
103177 for (i = 0; i < ln; i++) {
103178 slots[i].picker = this;
103179 }
103180 }
103181
103182 return slots;
103183 },
103184
103185 /**
103186 * Adds any new {@link #slots} to this picker, and removes existing {@link #slots}
103187 * @private
103188 */
103189 updateSlots: function(newSlots) {
103190 var bcss = Ext.baseCSSPrefix,
103191 innerItems;
103192
103193 this.removeAll();
103194
103195 if (newSlots) {
103196 this.add(newSlots);
103197 }
103198
103199 innerItems = this.getInnerItems();
103200 if (innerItems.length > 0) {
103201 innerItems[0].addCls(bcss + 'first');
103202 innerItems[innerItems.length - 1].addCls(bcss + 'last');
103203 }
103204
103205 this.updateUseTitles(this.getUseTitles());
103206 },
103207
103208 /**
103209 * @private
103210 * Called when the done button has been tapped.
103211 */
103212 onDoneButtonTap: function() {
103213 var oldValue = this._value,
103214 newValue = this.getValue(true);
103215
103216 if (newValue != oldValue) {
103217 this.fireEvent('change', this, newValue);
103218 }
103219
103220 this.hide();
103221 Ext.util.InputBlocker.unblockInputs();
103222 },
103223
103224 /**
103225 * @private
103226 * Called when the cancel button has been tapped.
103227 */
103228 onCancelButtonTap: function() {
103229 this.fireEvent('cancel', this);
103230 this.hide();
103231 Ext.util.InputBlocker.unblockInputs();
103232 },
103233
103234 /**
103235 * @private
103236 * Called when a slot has been picked.
103237 */
103238 onSlotPick: function(slot) {
103239 this.fireEvent('pick', this, this.getValue(true), slot);
103240 },
103241
103242 show: function() {
103243 if (this.getParent() === undefined) {
103244 Ext.Viewport.add(this);
103245 }
103246
103247 this.callParent(arguments);
103248
103249 if (!this.isHidden()) {
103250 this.setValue(this._value);
103251 }
103252 Ext.util.InputBlocker.blockInputs();
103253 },
103254
103255 /**
103256 * Sets the values of the pickers slots.
103257 * @param {Object} values The values in a {name:'value'} format.
103258 * @param {Boolean} animated `true` to animate setting the values.
103259 * @return {Ext.Picker} this This picker.
103260 */
103261 setValue: function(values, animated) {
103262 var me = this,
103263 slots = me.getInnerItems(),
103264 ln = slots.length,
103265 key, slot, loopSlot, i, value;
103266
103267 if (!values) {
103268 values = {};
103269 for (i = 0; i < ln; i++) {
103270 //set the value to false so the slot will return null when getValue is called
103271 values[slots[i].config.name] = null;
103272 }
103273 }
103274
103275 for (key in values) {
103276 slot = null;
103277 value = values[key];
103278 for (i = 0; i < slots.length; i++) {
103279 loopSlot = slots[i];
103280 if (loopSlot.config.name == key) {
103281 slot = loopSlot;
103282 break;
103283 }
103284 }
103285
103286 if (slot) {
103287 if (animated) {
103288 slot.setValueAnimated(value);
103289 } else {
103290 slot.setValue(value);
103291 }
103292 }
103293 }
103294
103295 me._values = me._value = values;
103296
103297 return me;
103298 },
103299
103300 setValueAnimated: function(values) {
103301 this.setValue(values, true);
103302 },
103303
103304 /**
103305 * Returns the values of each of the pickers slots
103306 * @return {Object} The values of the pickers slots
103307 */
103308 getValue: function(useDom) {
103309 var values = {},
103310 items = this.getItems().items,
103311 ln = items.length,
103312 item, i;
103313
103314 if (useDom) {
103315 for (i = 0; i < ln; i++) {
103316 item = items[i];
103317 if (item && item.isSlot) {
103318 values[item.getName()] = item.getValue(useDom);
103319 }
103320 }
103321
103322 this._values = values;
103323 }
103324
103325 return this._values;
103326 },
103327
103328 /**
103329 * Returns the values of each of the pickers slots.
103330 * @return {Object} The values of the pickers slots.
103331 */
103332 getValues: function() {
103333 return this.getValue();
103334 },
103335
103336 destroy: function() {
103337 this.callParent();
103338 Ext.destroy(this.mask, this.bar);
103339 }
103340 }, function() {
103341 });
103342
103343
103344 /**
103345 * Simple Select field wrapper. Example usage:
103346 *
103347 * @example
103348 * Ext.create('Ext.form.Panel', {
103349 * fullscreen: true,
103350 * items: [
103351 * {
103352 * xtype: 'fieldset',
103353 * title: 'Select',
103354 * items: [
103355 * {
103356 * xtype: 'selectfield',
103357 * label: 'Choose one',
103358 * options: [
103359 * {text: 'First Option', value: 'first'},
103360 * {text: 'Second Option', value: 'second'},
103361 * {text: 'Third Option', value: 'third'}
103362 * ]
103363 * }
103364 * ]
103365 * }
103366 * ]
103367 * });
103368 *
103369 * For more information regarding forms and fields, please review [Using Forms in Sencha Touch Guide](../../../components/forms.html)
103370 */
103371 Ext.define('Ext.field.Select', {
103372 extend: Ext.field.Text ,
103373 xtype: 'selectfield',
103374 alternateClassName: 'Ext.form.Select',
103375
103376
103377
103378
103379
103380
103381
103382
103383 /**
103384 * @event change
103385 * Fires when an option selection has changed
103386 * @param {Ext.field.Select} this
103387 * @param {Mixed} newValue The new value
103388 * @param {Mixed} oldValue The old value
103389 */
103390
103391 /**
103392 * @event focus
103393 * Fires when this field receives input focus. This happens both when you tap on the field and when you focus on the field by using
103394 * 'next' or 'tab' on a keyboard.
103395 *
103396 * Please note that this event is not very reliable on Android. For example, if your Select field is second in your form panel,
103397 * you cannot use the Next button to get to this select field. This functionality works as expected on iOS.
103398 * @param {Ext.field.Select} this This field
103399 * @param {Ext.event.Event} e
103400 */
103401
103402 config: {
103403 /**
103404 * @cfg
103405 * @inheritdoc
103406 */
103407 ui: 'select',
103408
103409 /**
103410 * @cfg {Boolean} useClearIcon
103411 * @hide
103412 */
103413
103414 /**
103415 * @cfg {String/Number} valueField The underlying {@link Ext.data.Field#name data value name} (or numeric Array index) to bind to this
103416 * Select control.
103417 * @accessor
103418 */
103419 valueField: 'value',
103420
103421 /**
103422 * @cfg {String/Number} displayField The underlying {@link Ext.data.Field#name data value name} (or numeric Array index) to bind to this
103423 * Select control. This resolved value is the visibly rendered value of the available selection options.
103424 * @accessor
103425 */
103426 displayField: 'text',
103427
103428 /**
103429 * @cfg {Ext.data.Store/Object/String} store The store to provide selection options data.
103430 * Either a Store instance, configuration object or store ID.
103431 * @accessor
103432 */
103433 store: null,
103434
103435 /**
103436 * @cfg {Array} options An array of select options.
103437 *
103438 * [
103439 * {text: 'First Option', value: 'first'},
103440 * {text: 'Second Option', value: 'second'},
103441 * {text: 'Third Option', value: 'third'}
103442 * ]
103443 *
103444 * __Note:__ Option object member names should correspond with defined {@link #valueField valueField} and {@link #displayField displayField} values.
103445 * This config will be ignored if a {@link #store store} instance is provided.
103446 * @accessor
103447 */
103448 options: null,
103449
103450 /**
103451 * @cfg {String} hiddenName Specify a `hiddenName` if you're using the {@link Ext.form.Panel#standardSubmit standardSubmit} option.
103452 * This name will be used to post the underlying value of the select to the server.
103453 * @accessor
103454 */
103455 hiddenName: null,
103456
103457 /**
103458 * @cfg {Object} component
103459 * @accessor
103460 * @hide
103461 */
103462 component: {
103463 useMask: true
103464 },
103465
103466 /**
103467 * @cfg {Boolean} clearIcon
103468 * @hide
103469 * @accessor
103470 */
103471 clearIcon: false,
103472
103473 /**
103474 * @cfg {String/Boolean} usePicker
103475 * `true` if you want this component to always use a {@link Ext.picker.Picker}.
103476 * `false` if you want it to use a popup overlay {@link Ext.List}.
103477 * `auto` if you want to show a {@link Ext.picker.Picker} only on phones.
103478 */
103479 usePicker: 'auto',
103480
103481 /**
103482 * @cfg {Boolean} autoSelect
103483 * `true` to auto select the first value in the {@link #store} or {@link #options} when they are changed. Only happens when
103484 * the {@link #value} is set to `null`.
103485 */
103486 autoSelect: true,
103487
103488 /**
103489 * @cfg {Object} defaultPhonePickerConfig
103490 * The default configuration for the picker component when you are on a phone.
103491 */
103492 defaultPhonePickerConfig: null,
103493
103494 /**
103495 * @cfg {Object} defaultTabletPickerConfig
103496 * The default configuration for the picker component when you are on a tablet.
103497 */
103498 defaultTabletPickerConfig: null,
103499
103500 /**
103501 * @cfg
103502 * @inheritdoc
103503 */
103504 name: 'picker',
103505
103506 /**
103507 * @cfg {String} pickerSlotAlign
103508 * The alignment of text in the picker created by this Select
103509 * @private
103510 */
103511 pickerSlotAlign: 'center'
103512 },
103513
103514 platformConfig: [
103515 {
103516 theme: ['Windows'],
103517 pickerSlotAlign: 'left'
103518 },
103519 {
103520 theme: ['Tizen'],
103521 usePicker: false
103522 }
103523 ],
103524
103525 // @private
103526 initialize: function() {
103527 var me = this,
103528 component = me.getComponent();
103529
103530 me.callParent();
103531
103532 component.on({
103533 scope: me,
103534 masktap: 'onMaskTap'
103535 });
103536
103537 component.doMaskTap = Ext.emptyFn;
103538
103539 if (Ext.browser.is.AndroidStock2) {
103540 component.input.dom.disabled = true;
103541 }
103542
103543 if (Ext.theme.is.Blackberry || Ext.theme.is.Blackberry103) {
103544 this.label.on({
103545 scope: me,
103546 tap: "onFocus"
103547 });
103548 }
103549 },
103550
103551 getElementConfig: function() {
103552 if (Ext.theme.is.Blackberry || Ext.theme.is.Blackberry103) {
103553 var prefix = Ext.baseCSSPrefix;
103554
103555 return {
103556 reference: 'element',
103557 className: 'x-container',
103558 children: [
103559 {
103560 reference: 'innerElement',
103561 cls: prefix + 'component-outer',
103562 children: [
103563 {
103564 reference: 'label',
103565 cls: prefix + 'form-label',
103566 children: [{
103567 reference: 'labelspan',
103568 tag: 'span'
103569 }]
103570 }
103571 ]
103572 }
103573 ]
103574 };
103575 } else {
103576 return this.callParent(arguments);
103577 }
103578 },
103579
103580 /**
103581 * @private
103582 */
103583 updateDefaultPhonePickerConfig: function(newConfig) {
103584 var picker = this.picker;
103585 if (picker) {
103586 picker.setConfig(newConfig);
103587 }
103588 },
103589
103590 /**
103591 * @private
103592 */
103593 updateDefaultTabletPickerConfig: function(newConfig) {
103594 var listPanel = this.listPanel;
103595 if (listPanel) {
103596 listPanel.setConfig(newConfig);
103597 }
103598 },
103599
103600 /**
103601 * @private
103602 * Checks if the value is `auto`. If it is, it only uses the picker if the current device type
103603 * is a phone.
103604 */
103605 applyUsePicker: function(usePicker) {
103606 if (usePicker == "auto") {
103607 usePicker = (Ext.os.deviceType == 'Phone');
103608 }
103609
103610 return Boolean(usePicker);
103611 },
103612
103613 syncEmptyCls: Ext.emptyFn,
103614
103615 /**
103616 * @private
103617 */
103618 applyValue: function(value) {
103619 var record = value,
103620 index, store;
103621
103622 //we call this so that the options configruation gets intiailized, so that a store exists, and we can
103623 //find the correct value
103624 this.getOptions();
103625
103626 store = this.getStore();
103627
103628 if ((value != undefined && !value.isModel) && store) {
103629 index = store.find(this.getValueField(), value, null, null, null, true);
103630
103631 if (index == -1) {
103632 index = store.find(this.getDisplayField(), value, null, null, null, true);
103633 }
103634
103635 record = store.getAt(index);
103636 }
103637
103638 return record;
103639 },
103640
103641 updateValue: function(newValue, oldValue) {
103642 this.record = newValue;
103643
103644 this.callParent([(newValue && newValue.isModel) ? newValue.get(this.getDisplayField()) : '']);
103645 },
103646
103647 getValue: function() {
103648 var record = this.record;
103649 return (record && record.isModel) ? record.get(this.getValueField()) : null;
103650 },
103651
103652 /**
103653 * Returns the current selected {@link Ext.data.Model record} instance selected in this field.
103654 * @return {Ext.data.Model} the record.
103655 */
103656 getRecord: function() {
103657 return this.record;
103658 },
103659
103660 // @private
103661 getPhonePicker: function() {
103662 var config = this.getDefaultPhonePickerConfig();
103663
103664 if (!this.picker) {
103665 this.picker = Ext.create('Ext.picker.Picker', Ext.apply({
103666 slots: [
103667 {
103668 align: this.getPickerSlotAlign(),
103669 name: this.getName(),
103670 valueField: this.getValueField(),
103671 displayField: this.getDisplayField(),
103672 value: this.getValue(),
103673 store: this.getStore()
103674 }
103675 ],
103676 listeners: {
103677 change: this.onPickerChange,
103678 scope: this
103679 }
103680 }, config));
103681 }
103682
103683 return this.picker;
103684 },
103685
103686 // @private
103687 getTabletPicker: function() {
103688 var config = this.getDefaultTabletPickerConfig();
103689
103690 if (!this.listPanel) {
103691 this.listPanel = Ext.create('Ext.Panel', Ext.apply({
103692 left: 0,
103693 top: 0,
103694 modal: true,
103695 cls: Ext.baseCSSPrefix + 'select-overlay',
103696 layout: 'fit',
103697 hideOnMaskTap: true,
103698 width: Ext.os.is.Phone ? '14em' : '18em',
103699 height: (Ext.os.is.BlackBerry && Ext.os.version.getMajor() === 10) ? '12em' : (Ext.os.is.Phone ? '12.5em' : '22em'),
103700 items: {
103701 xtype: 'list',
103702 store: this.getStore(),
103703 itemTpl: '<span class="x-list-label">{' + this.getDisplayField() + ':htmlEncode}</span>',
103704 listeners: {
103705 select: this.onListSelect,
103706 itemtap: this.onListTap,
103707 scope: this
103708 }
103709 }
103710 }, config));
103711 }
103712
103713 return this.listPanel;
103714 },
103715
103716 // @private
103717 onMaskTap: function() {
103718 this.onFocus();
103719
103720 return false;
103721 },
103722
103723 /**
103724 * Shows the picker for the select field, whether that is a {@link Ext.picker.Picker} or a simple
103725 * {@link Ext.List list}.
103726 */
103727 showPicker: function() {
103728 var me = this,
103729 store = me.getStore(),
103730 value = me.getValue();
103731
103732 //check if the store is empty, if it is, return
103733 if (!store || store.getCount() === 0) {
103734 return;
103735 }
103736
103737 if (me.getReadOnly()) {
103738 return;
103739 }
103740
103741 me.isFocused = true;
103742
103743 if (me.getUsePicker()) {
103744 var picker = me.getPhonePicker(),
103745 name = me.getName(),
103746 pickerValue = {};
103747
103748 pickerValue[name] = value;
103749 picker.setValue(pickerValue);
103750
103751 if (!picker.getParent()) {
103752 Ext.Viewport.add(picker);
103753 }
103754
103755 picker.show();
103756 } else {
103757 var listPanel = me.getTabletPicker(),
103758 list = listPanel.down('list'),
103759 index, record;
103760
103761 if (!listPanel.getParent()) {
103762 Ext.Viewport.add(listPanel);
103763 }
103764
103765 listPanel.showBy(me.getComponent(), null);
103766
103767 if (value || me.getAutoSelect()) {
103768 store = list.getStore();
103769 index = store.find(me.getValueField(), value, null, null, null, true);
103770 record = store.getAt(index);
103771
103772 if (record) {
103773 list.select(record, null, true);
103774 }
103775 }
103776 }
103777 },
103778
103779 // @private
103780 onListSelect: function(item, record) {
103781 var me = this;
103782 if (record) {
103783 me.setValue(record);
103784 }
103785 },
103786
103787 onListTap: function() {
103788 this.listPanel.hide({
103789 type: 'fade',
103790 out: true,
103791 scope: this
103792 });
103793 },
103794
103795 // @private
103796 onPickerChange: function(picker, value) {
103797 var me = this,
103798 newValue = value[me.getName()],
103799 store = me.getStore(),
103800 index = store.find(me.getValueField(), newValue, null, null, null, true),
103801 record = store.getAt(index);
103802
103803 me.setValue(record);
103804 },
103805
103806 onChange: function(component, newValue, oldValue) {
103807 var me = this,
103808 store = me.getStore(),
103809 index = (store) ? store.find(me.getDisplayField(), oldValue, null, null, null, true) : -1,
103810 valueField = me.getValueField(),
103811 record = (store) ? store.getAt(index) : null;
103812
103813 oldValue = (record) ? record.get(valueField) : null;
103814
103815 me.fireEvent('change', me, me.getValue(), oldValue);
103816 },
103817
103818 /**
103819 * Updates the underlying `<options>` list with new values.
103820 *
103821 * @param {Array} newOptions An array of options configurations to insert or append.
103822 *
103823 * selectBox.setOptions([
103824 * {text: 'First Option', value: 'first'},
103825 * {text: 'Second Option', value: 'second'},
103826 * {text: 'Third Option', value: 'third'}
103827 * ]).setValue('third');
103828 *
103829 * __Note:__ option object member names should correspond with defined {@link #valueField valueField} and
103830 * {@link #displayField displayField} values.
103831 *
103832 * @return {Ext.field.Select} this
103833 */
103834 updateOptions: function(newOptions) {
103835 var store = this.getStore();
103836
103837 if (!store) {
103838 this.setStore(true);
103839 store = this._store;
103840 }
103841
103842 if (!newOptions) {
103843 store.clearData();
103844 }
103845 else {
103846 store.setData(newOptions);
103847 this.onStoreDataChanged(store);
103848 }
103849 return this;
103850 },
103851
103852 applyStore: function(store) {
103853 if (store === true) {
103854 store = Ext.create('Ext.data.Store', {
103855 fields: [this.getValueField(), this.getDisplayField()],
103856 autoDestroy: true
103857 });
103858 }
103859
103860 if (store) {
103861 store = Ext.data.StoreManager.lookup(store);
103862
103863 store.on({
103864 scope: this,
103865 addrecords: 'onStoreDataChanged',
103866 removerecords: 'onStoreDataChanged',
103867 updaterecord: 'onStoreDataChanged',
103868 refresh: 'onStoreDataChanged'
103869 });
103870 }
103871
103872 return store;
103873 },
103874
103875 updateStore: function(newStore) {
103876 if (newStore) {
103877 this.onStoreDataChanged(newStore);
103878 }
103879
103880 if (this.getUsePicker() && this.picker) {
103881 this.picker.down('pickerslot').setStore(newStore);
103882 } else if (this.listPanel) {
103883 this.listPanel.down('dataview').setStore(newStore);
103884 }
103885 },
103886
103887 /**
103888 * Called when the internal {@link #store}'s data has changed.
103889 */
103890 onStoreDataChanged: function(store) {
103891 var initialConfig = this.getInitialConfig(),
103892 value = this.getValue();
103893
103894 if (value || value == 0) {
103895 this.updateValue(this.applyValue(value));
103896 }
103897
103898 if (this.getValue() === null) {
103899 if (initialConfig.hasOwnProperty('value')) {
103900 this.setValue(initialConfig.value);
103901 }
103902
103903 if (this.getValue() === null && this.getAutoSelect()) {
103904 if (store.getCount() > 0) {
103905 this.setValue(store.getAt(0));
103906 }
103907 }
103908 }
103909 },
103910
103911 /**
103912 * @private
103913 */
103914 doSetDisabled: function(disabled) {
103915 var component = this.getComponent();
103916 if (component) {
103917 component.setDisabled(disabled);
103918 }
103919 Ext.Component.prototype.doSetDisabled.apply(this, arguments);
103920 },
103921
103922 /**
103923 * @private
103924 */
103925 setDisabled: function() {
103926 Ext.Component.prototype.setDisabled.apply(this, arguments);
103927 },
103928
103929 // @private
103930 updateLabelWidth: function() {
103931 if (Ext.theme.is.Blackberry || Ext.theme.is.Blackberry103) {
103932 return;
103933 } else {
103934 this.callParent(arguments);
103935 }
103936 },
103937
103938 // @private
103939 updateLabelAlign: function() {
103940 if (Ext.theme.is.Blackberry || Ext.theme.is.Blackberry103) {
103941 return;
103942 } else {
103943 this.callParent(arguments);
103944 }
103945 },
103946
103947 /**
103948 * Resets the Select field to the value of the first record in the store.
103949 * @return {Ext.field.Select} this
103950 * @chainable
103951 */
103952 reset: function() {
103953 var me = this,
103954 record;
103955
103956 if (me.getAutoSelect()) {
103957 var store = me.getStore();
103958
103959 record = (me.originalValue) ? me.originalValue : store.getAt(0);
103960 } else {
103961 var usePicker = me.getUsePicker(),
103962 picker = usePicker ? me.picker : me.listPanel;
103963
103964 if (picker) {
103965 picker = picker.child(usePicker ? 'pickerslot' : 'dataview');
103966
103967 picker.deselectAll();
103968 }
103969
103970 record = null;
103971 }
103972
103973 me.setValue(record);
103974
103975 return me;
103976 },
103977
103978 onFocus: function(e) {
103979 if (this.getDisabled()) {
103980 return false;
103981 }
103982
103983 var component = this.getComponent();
103984 this.fireEvent('focus', this, e);
103985
103986 if (Ext.os.is.Android4) {
103987 component.input.dom.focus();
103988 }
103989 component.input.dom.blur();
103990
103991 this.isFocused = true;
103992
103993 this.showPicker();
103994 },
103995
103996 destroy: function() {
103997 this.callParent(arguments);
103998 var store = this.getStore();
103999
104000 if (store && store.getAutoDestroy()) {
104001 Ext.destroy(store);
104002 }
104003
104004 Ext.destroy(this.listPanel, this.picker);
104005 }
104006 });
104007
104008 /**
104009 * A date picker component which shows a Date Picker on the screen. This class extends from {@link Ext.picker.Picker}
104010 * and {@link Ext.Sheet} so it is a popup.
104011 *
104012 * This component has no required configurations.
104013 *
104014 * ## Examples
104015 *
104016 * @example miniphone preview
104017 * var datePicker = Ext.create('Ext.picker.Date');
104018 * Ext.Viewport.add(datePicker);
104019 * datePicker.show();
104020 *
104021 * You may want to adjust the {@link #yearFrom} and {@link #yearTo} properties:
104022 *
104023 * @example miniphone preview
104024 * var datePicker = Ext.create('Ext.picker.Date', {
104025 * yearFrom: 2000,
104026 * yearTo : 2015
104027 * });
104028 * Ext.Viewport.add(datePicker);
104029 * datePicker.show();
104030 *
104031 * You can set the value of the {@link Ext.picker.Date} to the current date using `new Date()`:
104032 *
104033 * @example miniphone preview
104034 * var datePicker = Ext.create('Ext.picker.Date', {
104035 * value: new Date()
104036 * });
104037 * Ext.Viewport.add(datePicker);
104038 * datePicker.show();
104039 *
104040 * And you can hide the titles from each of the slots by using the {@link #useTitles} configuration:
104041 *
104042 * @example miniphone preview
104043 * var datePicker = Ext.create('Ext.picker.Date', {
104044 * useTitles: false
104045 * });
104046 * Ext.Viewport.add(datePicker);
104047 * datePicker.show();
104048 */
104049 Ext.define('Ext.picker.Date', {
104050 extend: Ext.picker.Picker ,
104051 xtype: 'datepicker',
104052 alternateClassName: 'Ext.DatePicker',
104053
104054
104055 /**
104056 * @event change
104057 * Fired when the value of this picker has changed and the done button is pressed.
104058 * @param {Ext.picker.Date} this This Picker
104059 * @param {Date} value The date value
104060 */
104061
104062 config: {
104063 /**
104064 * @cfg {Number} yearFrom
104065 * The start year for the date picker. If {@link #yearFrom} is greater than
104066 * {@link #yearTo} then the order of years will be reversed.
104067 * @accessor
104068 */
104069 yearFrom: 1980,
104070
104071 /**
104072 * @cfg {Number} [yearTo=new Date().getFullYear()]
104073 * The last year for the date picker. If {@link #yearFrom} is greater than
104074 * {@link #yearTo} then the order of years will be reversed.
104075 * @accessor
104076 */
104077 yearTo: new Date().getFullYear(),
104078
104079 /**
104080 * @cfg {String} monthText
104081 * The label to show for the month column.
104082 * @accessor
104083 */
104084 monthText: 'Month',
104085
104086 /**
104087 * @cfg {String} dayText
104088 * The label to show for the day column.
104089 * @accessor
104090 */
104091 dayText: 'Day',
104092
104093 /**
104094 * @cfg {String} yearText
104095 * The label to show for the year column.
104096 * @accessor
104097 */
104098 yearText: 'Year',
104099
104100 /**
104101 * @cfg {Array} slotOrder
104102 * An array of strings that specifies the order of the slots.
104103 * @accessor
104104 */
104105 slotOrder: ['month', 'day', 'year'],
104106
104107 /**
104108 * @cfg {Object/Date} value
104109 * Default value for the field and the internal {@link Ext.picker.Date} component. Accepts an object of 'year',
104110 * 'month' and 'day' values, all of which should be numbers, or a {@link Date}.
104111 *
104112 * Examples:
104113 *
104114 * - `{year: 1989, day: 1, month: 5}` = 1st May 1989
104115 * - `new Date()` = current date
104116 * @accessor
104117 */
104118
104119 /**
104120 * @cfg {Array} slots
104121 * @hide
104122 * @accessor
104123 */
104124
104125 /**
104126 * @cfg {String/Mixed} doneButton
104127 * Can be either:
104128 *
104129 * - A {String} text to be used on the Done button.
104130 * - An {Object} as config for {@link Ext.Button}.
104131 * - `false` or `null` to hide it.
104132 * @accessor
104133 */
104134 doneButton: true
104135 },
104136
104137 platformConfig: [{
104138 theme: ['Windows'],
104139 doneButton: {
104140 iconCls: 'check2',
104141 ui: 'round',
104142 text: ''
104143 }
104144 }],
104145
104146 initialize: function() {
104147 this.callParent();
104148
104149 this.on({
104150 scope: this,
104151 delegate: '> slot',
104152 slotpick: this.onSlotPick
104153 });
104154
104155 this.on({
104156 scope: this,
104157 show: this.onSlotPick
104158 });
104159 },
104160
104161 setValue: function(value, animated) {
104162 if (Ext.isDate(value)) {
104163 value = {
104164 day : value.getDate(),
104165 month: value.getMonth() + 1,
104166 year : value.getFullYear()
104167 };
104168 }
104169
104170 this.callParent([value, animated]);
104171 this.onSlotPick();
104172 },
104173
104174 getValue: function(useDom) {
104175 var values = {},
104176 items = this.getItems().items,
104177 ln = items.length,
104178 daysInMonth, day, month, year, item, i;
104179
104180 for (i = 0; i < ln; i++) {
104181 item = items[i];
104182 if (item instanceof Ext.picker.Slot) {
104183 values[item.getName()] = item.getValue(useDom);
104184 }
104185 }
104186
104187 //if all the slots return null, we should not return a date
104188 if (values.year === null && values.month === null && values.day === null) {
104189 return null;
104190 }
104191
104192 year = Ext.isNumber(values.year) ? values.year : 1;
104193 month = Ext.isNumber(values.month) ? values.month : 1;
104194 day = Ext.isNumber(values.day) ? values.day : 1;
104195
104196 if (month && year && month && day) {
104197 daysInMonth = this.getDaysInMonth(month, year);
104198 }
104199 day = (daysInMonth) ? Math.min(day, daysInMonth): day;
104200
104201 return new Date(year, month - 1, day);
104202 },
104203
104204 /**
104205 * Updates the yearFrom configuration
104206 */
104207 updateYearFrom: function() {
104208 if (this.initialized) {
104209 this.createSlots();
104210 }
104211 },
104212
104213 /**
104214 * Updates the yearTo configuration
104215 */
104216 updateYearTo: function() {
104217 if (this.initialized) {
104218 this.createSlots();
104219 }
104220 },
104221
104222 /**
104223 * Updates the monthText configuration
104224 */
104225 updateMonthText: function(newMonthText, oldMonthText) {
104226 var innerItems = this.getInnerItems,
104227 ln = innerItems.length,
104228 item, i;
104229
104230 //loop through each of the current items and set the title on the correct slice
104231 if (this.initialized) {
104232 for (i = 0; i < ln; i++) {
104233 item = innerItems[i];
104234
104235 if ((typeof item.title == "string" && item.title == oldMonthText) || (item.title.html == oldMonthText)) {
104236 item.setTitle(newMonthText);
104237 }
104238 }
104239 }
104240 },
104241
104242 /**
104243 * Updates the {@link #dayText} configuration.
104244 */
104245 updateDayText: function(newDayText, oldDayText) {
104246 var innerItems = this.getInnerItems,
104247 ln = innerItems.length,
104248 item, i;
104249
104250 //loop through each of the current items and set the title on the correct slice
104251 if (this.initialized) {
104252 for (i = 0; i < ln; i++) {
104253 item = innerItems[i];
104254
104255 if ((typeof item.title == "string" && item.title == oldDayText) || (item.title.html == oldDayText)) {
104256 item.setTitle(newDayText);
104257 }
104258 }
104259 }
104260 },
104261
104262 /**
104263 * Updates the yearText configuration
104264 */
104265 updateYearText: function(yearText) {
104266 var innerItems = this.getInnerItems,
104267 ln = innerItems.length,
104268 item, i;
104269
104270 //loop through each of the current items and set the title on the correct slice
104271 if (this.initialized) {
104272 for (i = 0; i < ln; i++) {
104273 item = innerItems[i];
104274
104275 if (item.title == this.yearText) {
104276 item.setTitle(yearText);
104277 }
104278 }
104279 }
104280 },
104281
104282 // @private
104283 constructor: function() {
104284 this.callParent(arguments);
104285 this.createSlots();
104286 },
104287
104288 /**
104289 * Generates all slots for all years specified by this component, and then sets them on the component
104290 * @private
104291 */
104292 createSlots: function() {
104293 var me = this,
104294 slotOrder = me.getSlotOrder(),
104295 yearsFrom = me.getYearFrom(),
104296 yearsTo = me.getYearTo(),
104297 years = [],
104298 days = [],
104299 months = [],
104300 reverse = yearsFrom > yearsTo,
104301 ln, i, daysInMonth;
104302
104303 while (yearsFrom) {
104304 years.push({
104305 text : yearsFrom,
104306 value : yearsFrom
104307 });
104308
104309 if (yearsFrom === yearsTo) {
104310 break;
104311 }
104312
104313 if (reverse) {
104314 yearsFrom--;
104315 } else {
104316 yearsFrom++;
104317 }
104318 }
104319
104320 daysInMonth = me.getDaysInMonth(1, new Date().getFullYear());
104321
104322 for (i = 0; i < daysInMonth; i++) {
104323 days.push({
104324 text : i + 1,
104325 value : i + 1
104326 });
104327 }
104328
104329 for (i = 0, ln = Ext.Date.monthNames.length; i < ln; i++) {
104330 months.push({
104331 text : Ext.Date.monthNames[i],
104332 value : i + 1
104333 });
104334 }
104335
104336 var slots = [];
104337
104338 slotOrder.forEach(function (item) {
104339 slots.push(me.createSlot(item, days, months, years));
104340 });
104341
104342 me.setSlots(slots);
104343 },
104344
104345 /**
104346 * Returns a slot config for a specified date.
104347 * @private
104348 */
104349 createSlot: function(name, days, months, years) {
104350 switch (name) {
104351 case 'year':
104352 return {
104353 name: 'year',
104354 align: 'center',
104355 data: years,
104356 title: this.getYearText(),
104357 flex: 3
104358 };
104359 case 'month':
104360 return {
104361 name: name,
104362 align: 'right',
104363 data: months,
104364 title: this.getMonthText(),
104365 flex: 4
104366 };
104367 case 'day':
104368 return {
104369 name: 'day',
104370 align: 'center',
104371 data: days,
104372 title: this.getDayText(),
104373 flex: 2
104374 };
104375 }
104376 },
104377
104378 onSlotPick: function() {
104379 var value = this.getValue(true),
104380 slot = this.getDaySlot(),
104381 year = value.getFullYear(),
104382 month = value.getMonth(),
104383 days = [],
104384 daysInMonth, i;
104385
104386 if (!value || !Ext.isDate(value) || !slot) {
104387 return;
104388 }
104389
104390 this.callParent(arguments);
104391
104392 //get the new days of the month for this new date
104393 daysInMonth = this.getDaysInMonth(month + 1, year);
104394 for (i = 0; i < daysInMonth; i++) {
104395 days.push({
104396 text: i + 1,
104397 value: i + 1
104398 });
104399 }
104400
104401 // We don't need to update the slot days unless it has changed
104402 if (slot.getStore().getCount() == days.length) {
104403 return;
104404 }
104405
104406 slot.getStore().setData(days);
104407
104408 // Now we have the correct amount of days for the day slot, lets update it
104409 var store = slot.getStore(),
104410 viewItems = slot.getViewItems(),
104411 valueField = slot.getValueField(),
104412 index, item;
104413
104414 index = store.find(valueField, value.getDate());
104415 if (index == -1) {
104416 return;
104417 }
104418
104419 item = Ext.get(viewItems[index]);
104420
104421 slot.selectedIndex = index;
104422 slot.scrollToItem(item);
104423 slot.setValue(slot.getValue(true));
104424 },
104425
104426 getDaySlot: function() {
104427 var innerItems = this.getInnerItems(),
104428 ln = innerItems.length,
104429 i, slot;
104430
104431 if (this.daySlot) {
104432 return this.daySlot;
104433 }
104434
104435 for (i = 0; i < ln; i++) {
104436 slot = innerItems[i];
104437 if (slot.isSlot && slot.getName() == "day") {
104438 this.daySlot = slot;
104439 return slot;
104440 }
104441 }
104442
104443 return null;
104444 },
104445
104446 // @private
104447 getDaysInMonth: function(month, year) {
104448 var daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
104449 return month == 2 && this.isLeapYear(year) ? 29 : daysInMonth[month-1];
104450 },
104451
104452 // @private
104453 isLeapYear: function(year) {
104454 return !!((year & 3) === 0 && (year % 100 || (year % 400 === 0 && year)));
104455 },
104456
104457 onDoneButtonTap: function() {
104458 var oldValue = this._value,
104459 newValue = this.getValue(true),
104460 testValue = newValue;
104461
104462 if (Ext.isDate(newValue)) {
104463 testValue = newValue.toDateString();
104464 }
104465 if (Ext.isDate(oldValue)) {
104466 oldValue = oldValue.toDateString();
104467 }
104468
104469 if (testValue != oldValue) {
104470 this.fireEvent('change', this, newValue);
104471 }
104472
104473 this.hide();
104474 Ext.util.InputBlocker.unblockInputs();
104475 }
104476 });
104477
104478 /**
104479 * This is a specialized field which shows a {@link Ext.picker.Date} when tapped. If it has a predefined value,
104480 * or a value is selected in the {@link Ext.picker.Date}, it will be displayed like a normal {@link Ext.field.Text}
104481 * (but not selectable/changable).
104482 *
104483 * Ext.create('Ext.field.DatePicker', {
104484 * label: 'Birthday',
104485 * value: new Date()
104486 * });
104487 *
104488 * {@link Ext.field.DatePicker} fields are very simple to implement, and have no required configurations.
104489 *
104490 * For more information regarding forms and fields, please review [Using Forms in Sencha Touch Guide](../../../components/forms.html)
104491 *
104492 * ## Examples
104493 *
104494 * It can be very useful to set a default {@link #value} configuration on {@link Ext.field.DatePicker} fields. In
104495 * this example, we set the {@link #value} to be the current date. You can also use the {@link #setValue} method to
104496 * update the value at any time.
104497 *
104498 * @example miniphone preview
104499 * Ext.create('Ext.form.Panel', {
104500 * fullscreen: true,
104501 * items: [
104502 * {
104503 * xtype: 'fieldset',
104504 * items: [
104505 * {
104506 * xtype: 'datepickerfield',
104507 * label: 'Birthday',
104508 * name: 'birthday',
104509 * value: new Date()
104510 * }
104511 * ]
104512 * },
104513 * {
104514 * xtype: 'toolbar',
104515 * docked: 'bottom',
104516 * items: [
104517 * { xtype: 'spacer' },
104518 * {
104519 * text: 'setValue',
104520 * handler: function() {
104521 * var datePickerField = Ext.ComponentQuery.query('datepickerfield')[0];
104522 *
104523 * var randomNumber = function(from, to) {
104524 * return Math.floor(Math.random() * (to - from + 1) + from);
104525 * };
104526 *
104527 * datePickerField.setValue({
104528 * month: randomNumber(0, 11),
104529 * day : randomNumber(0, 28),
104530 * year : randomNumber(1980, 2011)
104531 * });
104532 * }
104533 * },
104534 * { xtype: 'spacer' }
104535 * ]
104536 * }
104537 * ]
104538 * });
104539 *
104540 * When you need to retrieve the date from the {@link Ext.field.DatePicker}, you can either use the {@link #getValue} or
104541 * {@link #getFormattedValue} methods:
104542 *
104543 * @example preview
104544 * Ext.create('Ext.form.Panel', {
104545 * fullscreen: true,
104546 * items: [
104547 * {
104548 * xtype: 'fieldset',
104549 * items: [
104550 * {
104551 * xtype: 'datepickerfield',
104552 * label: 'Birthday',
104553 * name: 'birthday',
104554 * value: new Date()
104555 * }
104556 * ]
104557 * },
104558 * {
104559 * xtype: 'toolbar',
104560 * docked: 'bottom',
104561 * items: [
104562 * {
104563 * text: 'getValue',
104564 * handler: function() {
104565 * var datePickerField = Ext.ComponentQuery.query('datepickerfield')[0];
104566 * Ext.Msg.alert(null, datePickerField.getValue());
104567 * }
104568 * },
104569 * { xtype: 'spacer' },
104570 * {
104571 * text: 'getFormattedValue',
104572 * handler: function() {
104573 * var datePickerField = Ext.ComponentQuery.query('datepickerfield')[0];
104574 * Ext.Msg.alert(null, datePickerField.getFormattedValue());
104575 * }
104576 * }
104577 * ]
104578 * }
104579 * ]
104580 * });
104581 *
104582 *
104583 */
104584 Ext.define('Ext.field.DatePicker', {
104585 extend: Ext.field.Select ,
104586 alternateClassName: 'Ext.form.DatePicker',
104587 xtype: 'datepickerfield',
104588
104589
104590
104591
104592
104593 /**
104594 * @event change
104595 * Fires when a date is selected
104596 * @param {Ext.field.DatePicker} this
104597 * @param {Date} newDate The new date
104598 * @param {Date} oldDate The old date
104599 */
104600
104601 config: {
104602 ui: 'select',
104603
104604 /**
104605 * @cfg {Object/Ext.picker.Date} picker
104606 * An object that is used when creating the internal {@link Ext.picker.Date} component or a direct instance of {@link Ext.picker.Date}.
104607 * @accessor
104608 */
104609 picker: true,
104610
104611 /**
104612 * @cfg {Boolean}
104613 * @hide
104614 * @accessor
104615 */
104616 clearIcon: false,
104617
104618 /**
104619 * @cfg {Object/Date} value
104620 * Default value for the field and the internal {@link Ext.picker.Date} component. Accepts an object of 'year',
104621 * 'month' and 'day' values, all of which should be numbers, or a {@link Date}.
104622 *
104623 * Example: {year: 1989, day: 1, month: 5} = 1st May 1989 or new Date()
104624 * @accessor
104625 */
104626
104627 /**
104628 * @cfg {Boolean} destroyPickerOnHide
104629 * Whether or not to destroy the picker widget on hide. This save memory if it's not used frequently,
104630 * but increase delay time on the next show due to re-instantiation.
104631 * @accessor
104632 */
104633 destroyPickerOnHide: false,
104634
104635 /**
104636 * @cfg {String} [dateFormat=Ext.util.Format.defaultDateFormat] The format to be used when displaying the date in this field.
104637 * Accepts any valid date format. You can view formats over in the {@link Ext.Date} documentation.
104638 */
104639 dateFormat: null,
104640
104641 /**
104642 * @cfg {Object}
104643 * @hide
104644 */
104645 component: {
104646 useMask: true
104647 }
104648 },
104649
104650 initialize: function() {
104651 var me = this,
104652 component = me.getComponent();
104653
104654 me.callParent();
104655
104656 component.on({
104657 scope: me,
104658 masktap: 'onMaskTap'
104659 });
104660
104661
104662 component.doMaskTap = Ext.emptyFn;
104663
104664 if (Ext.browser.is.AndroidStock2) {
104665 component.input.dom.disabled = true;
104666 }
104667 },
104668
104669 syncEmptyCls: Ext.emptyFn,
104670
104671 applyValue: function(value) {
104672 if (!Ext.isDate(value) && !Ext.isObject(value)) {
104673 return null;
104674 }
104675
104676 if (Ext.isObject(value)) {
104677 return new Date(value.year, value.month - 1, value.day);
104678 }
104679
104680 return value;
104681 },
104682
104683 updateValue: function(newValue, oldValue) {
104684 var me = this,
104685 picker = me._picker;
104686
104687 if (picker && picker.isPicker) {
104688 picker.setValue(newValue);
104689 }
104690
104691 // Ext.Date.format expects a Date
104692 if (newValue !== null) {
104693 me.getComponent().setValue(Ext.Date.format(newValue, me.getDateFormat() || Ext.util.Format.defaultDateFormat));
104694 } else {
104695 me.getComponent().setValue('');
104696 }
104697
104698 if (newValue !== oldValue) {
104699 me.fireEvent('change', me, newValue, oldValue);
104700 }
104701 },
104702
104703 /**
104704 * Updates the date format in the field.
104705 * @private
104706 */
104707 updateDateFormat: function(newDateFormat, oldDateFormat) {
104708 var value = this.getValue();
104709 if (newDateFormat != oldDateFormat && Ext.isDate(value)) {
104710 this.getComponent().setValue(Ext.Date.format(value, newDateFormat || Ext.util.Format.defaultDateFormat));
104711 }
104712 },
104713
104714 /**
104715 * Returns the {@link Date} value of this field.
104716 * If you wanted a formatted date use the {@link #getFormattedValue} method.
104717 * @return {Date} The date selected
104718 */
104719 getValue: function() {
104720 if (this._picker && this._picker instanceof Ext.picker.Date) {
104721 return this._picker.getValue();
104722 }
104723
104724 return this._value;
104725 },
104726
104727 /**
104728 * Returns the value of the field formatted using the specified format. If it is not specified, it will default to
104729 * {@link #dateFormat} and then {@link Ext.util.Format#defaultDateFormat}.
104730 * @param {String} format The format to be returned.
104731 * @return {String} The formatted date.
104732 */
104733 getFormattedValue: function(format) {
104734 var value = this.getValue();
104735 return (Ext.isDate(value)) ? Ext.Date.format(value, format || this.getDateFormat() || Ext.util.Format.defaultDateFormat) : value;
104736 },
104737
104738 applyPicker: function(picker, pickerInstance) {
104739 if (pickerInstance && pickerInstance.isPicker) {
104740 picker = pickerInstance.setConfig(picker);
104741 }
104742
104743 return picker;
104744 },
104745
104746 getPicker: function() {
104747 var picker = this._picker,
104748 value = this.getValue();
104749
104750 if (picker && !picker.isPicker) {
104751 picker = Ext.factory(picker, Ext.picker.Date);
104752 if (value != null) {
104753 picker.setValue(value);
104754 }
104755 }
104756
104757 picker.on({
104758 scope: this,
104759 change: 'onPickerChange',
104760 hide : 'onPickerHide'
104761 });
104762
104763 this._picker = picker;
104764
104765 return picker;
104766 },
104767
104768 /**
104769 * @private
104770 * Listener to the tap event of the mask element. Shows the internal DatePicker component when the button has been tapped.
104771 */
104772 onMaskTap: function() {
104773 if (this.getDisabled()) {
104774 return false;
104775 }
104776
104777 this.onFocus();
104778
104779 return false;
104780 },
104781
104782 /**
104783 * Called when the picker changes its value.
104784 * @param {Ext.picker.Date} picker The date picker.
104785 * @param {Object} value The new value from the date picker.
104786 * @private
104787 */
104788 onPickerChange: function(picker, value) {
104789 var me = this,
104790 oldValue = me.getValue();
104791
104792 me.setValue(value);
104793 me.fireEvent('select', me, value);
104794 me.onChange(me, value, oldValue);
104795 },
104796
104797 /**
104798 * Override this or change event will be fired twice. change event is fired in updateValue
104799 * for this field. TOUCH-2861
104800 */
104801 onChange: Ext.emptyFn,
104802
104803 /**
104804 * Destroys the picker when it is hidden, if
104805 * {@link Ext.field.DatePicker#destroyPickerOnHide destroyPickerOnHide} is set to `true`.
104806 * @private
104807 */
104808 onPickerHide: function() {
104809 var me = this,
104810 picker = me.getPicker();
104811
104812 if (me.getDestroyPickerOnHide() && picker) {
104813 picker.destroy();
104814 me._picker = me.getInitialConfig().picker || true;
104815 }
104816 },
104817
104818 reset: function() {
104819 this.setValue(this.originalValue);
104820 },
104821
104822 onFocus: function(e) {
104823 var component = this.getComponent();
104824 this.fireEvent('focus', this, e);
104825
104826 if (Ext.os.is.Android4) {
104827 component.input.dom.focus();
104828 }
104829 component.input.dom.blur();
104830
104831 if (this.getReadOnly()) {
104832 return false;
104833 }
104834
104835 this.isFocused = true;
104836
104837 this.getPicker().show();
104838 },
104839
104840 // @private
104841 destroy: function() {
104842 var picker = this._picker;
104843
104844 if (picker && picker.isPicker) {
104845 picker.destroy();
104846 }
104847
104848 this.callParent(arguments);
104849 }
104850 });
104851
104852 Ext.define('Ext.field.DatePickerNative', {
104853 extend: Ext.field.DatePicker ,
104854 alternateClassName: 'Ext.form.DatePickerNative',
104855 xtype: 'datepickernativefield',
104856
104857 initialize: function() {
104858
104859 this.callParent();
104860
104861 },
104862
104863 onFocus: function(e) {
104864 var me = this;
104865
104866 if (!(navigator.plugins && navigator.plugins.dateTimePicker)){
104867
104868 me.callParent();
104869 return;
104870 }
104871
104872 var success = function (res) {
104873 me.setValue(res);
104874 };
104875
104876 var fail = function (e) {
104877 console.log("DateTimePicker: error occurred or cancelled: " + e);
104878 };
104879
104880 try {
104881
104882 var dateTimePickerFunc = me.getName() == 'date' ? navigator.plugins.dateTimePicker.selectDate :
104883 navigator.plugins.dateTimePicker.selectTime;
104884
104885 dateTimePickerFunc(success, fail, { value: me.getValue()});
104886
104887 } catch (ex) {
104888 fail(ex);
104889 }
104890 }
104891 });
104892
104893 /**
104894 * The Email field creates an HTML5 email input and is usually created inside a form. Because it creates an HTML email
104895 * input field, most browsers will show a specialized virtual keyboard for email address input. Aside from that, the
104896 * email field is just a normal text field. Here's an example of how to use it in a form:
104897 *
104898 * @example
104899 * Ext.create('Ext.form.Panel', {
104900 * fullscreen: true,
104901 * items: [
104902 * {
104903 * xtype: 'fieldset',
104904 * title: 'Register',
104905 * items: [
104906 * {
104907 * xtype: 'emailfield',
104908 * label: 'Email',
104909 * name: 'email'
104910 * },
104911 * {
104912 * xtype: 'passwordfield',
104913 * label: 'Password',
104914 * name: 'password'
104915 * }
104916 * ]
104917 * }
104918 * ]
104919 * });
104920 *
104921 * Or on its own, outside of a form:
104922 *
104923 * Ext.create('Ext.field.Email', {
104924 * label: 'Email address',
104925 * value: 'prefilled@email.com'
104926 * });
104927 *
104928 * Because email field inherits from {@link Ext.field.Text textfield} it gains all of the functionality that text fields
104929 * provide, including getting and setting the value at runtime, validations and various events that are fired as the
104930 * user interacts with the component. Check out the {@link Ext.field.Text} docs to see the additional functionality
104931 * available.
104932 *
104933 * For more information regarding forms and fields, please review [Using Forms in Sencha Touch Guide](../../../components/forms.html)
104934 */
104935 Ext.define('Ext.field.Email', {
104936 extend: Ext.field.Text ,
104937 alternateClassName: 'Ext.form.Email',
104938 xtype: 'emailfield',
104939
104940 config: {
104941 /**
104942 * @cfg
104943 * @inheritdoc
104944 */
104945 component: {
104946 type: 'email'
104947 },
104948
104949 /**
104950 * @cfg
104951 * @inheritdoc
104952 */
104953 autoCapitalize: false
104954 }
104955 });
104956
104957
104958
104959
104960
104961
104962 /**
104963 * @private
104964 */
104965 Ext.define('Ext.field.FileInput', {
104966 extend: Ext.field.Input ,
104967 xtype: 'fileinput',
104968
104969 /**
104970 * @event change
104971 * Fires just before the field blurs if the field value has changed
104972 * @param {Ext.field.Text} this This field
104973 * @param {Mixed} newValue The new value
104974 * @param {Mixed} oldValue The original value
104975 */
104976
104977 config: {
104978 type: "file",
104979 accept: null,
104980 capture: null,
104981 name: null,
104982 multiple: false
104983 },
104984
104985 /**
104986 * @property {Object} Lookup of capture devices to accept types
104987 * @private
104988 */
104989 captureLookup: {
104990 video: "camcorder",
104991 image: "camera",
104992 audio: "microphone"
104993 },
104994
104995 // @private
104996 initialize: function() {
104997 var me = this;
104998
104999 me.callParent();
105000
105001 me.input.on({
105002 scope: me,
105003 change: 'onInputChange'
105004 });
105005 },
105006
105007 /**
105008 * Returns the field data value.
105009 * @return {String} value The field value.
105010 */
105011 getValue: function() {
105012 var input = this.input;
105013
105014 if (input) {
105015 this._value = input.dom.value;
105016 }
105017
105018 return this._value;
105019 },
105020
105021 /**
105022 * Sets the internal value. Security restrictions prevent setting file values on the input element
105023 * @cfg newValue {string} New Value
105024 * @returns {String}
105025 */
105026 setValue: function(newValue) {
105027 var oldValue = this._value;
105028 this._value = newValue;
105029
105030 if (String(this._value) != String(oldValue) && this.initialized) {
105031 this.onChange(this, this._value, oldValue);
105032 }
105033
105034 return this;
105035 },
105036
105037 /**
105038 * Returns the field files.
105039 * @return {FileList} List of the files selected.
105040 */
105041 getFiles: function() {
105042 var input = this.input;
105043
105044 if (input) {
105045 this.$files = input.dom.files;
105046 }
105047
105048 return this.$files;
105049 },
105050
105051 // @private
105052 onInputChange: function(e) {
105053 this.setValue(e.target.value);
105054 },
105055
105056 /**
105057 * Called when the value changes on this input item
105058 * @cfg me {Ext.field.FileInput}
105059 * @cfg value {String} new Value
105060 * @cfg startValue {String} Original Value
105061 */
105062 onChange: function(me, value, startValue) {
105063 this.fireEvent('change', me, value, startValue);
105064 },
105065
105066 /**
105067 * Called when the name being changed
105068 * @cfg value new value
105069 * @returns {*}
105070 */
105071 applyName: function(value) {
105072 if(this.getMultiple() && value.substr(-2, 2) !== "[]") {
105073 value += "[]";
105074 }else if((!this.getMultiple()) && value.substr(-2, 2) === "[]") {
105075 value = value.substr(0, value.length-2)
105076 }
105077
105078 return value;
105079 },
105080
105081 /**
105082 * Applies the multiple attribute to the input
105083 * @cfg value {boolean}
105084 * @returns {boolean}
105085 */
105086 applyMultiple: function(value) {
105087 this.updateFieldAttribute('multiple', value ? '' : null);
105088 return value;
105089 },
105090
105091 /**
105092 * Called when the multiple property is updated. The name will automatically be toggled to an array if needed.
105093 */
105094 updateMultiple: function() {
105095 var name = this.getName();
105096 if(!Ext.isEmpty(name)) {
105097 this.setName(name);
105098 }
105099 },
105100
105101 /*
105102 * Updates the accept attribute with the {@link #accept} configuration.
105103 *
105104 */
105105 applyAccept: function(value) {
105106 switch (value) {
105107 case "video":
105108 case "audio":
105109 case "image":
105110 value = value + "/*";
105111 break;
105112 }
105113
105114 this.updateFieldAttribute('accept', value);
105115 },
105116
105117 /**
105118 * Updated the capture attribute with the {@ink capture} configuration
105119 */
105120 applyCapture: function(value) {
105121 this.updateFieldAttribute('capture', value);
105122 return value;
105123 }
105124 });
105125
105126 /**
105127 * Creates an HTML file input field on the page. This is usually used to upload files to remote server. File fields are usually
105128 * created inside a form like this:
105129 *
105130 * @example
105131 * Ext.create('Ext.form.Panel', {
105132 * fullscreen: true,
105133 * items: [
105134 * {
105135 * xtype: 'fieldset',
105136 * title: 'My Uploader',
105137 * items: [
105138 * {
105139 * xtype: 'filefield',
105140 * label: "MyPhoto:",
105141 * name: 'photo',
105142 * accept: 'image'
105143 * }
105144 * ]
105145 * }
105146 * ]
105147 * });
105148 *
105149 * For more information regarding forms and fields, please review [Using Forms in Sencha Touch Guide](../../../components/forms.html)
105150 */
105151
105152 Ext.define('Ext.field.File', {
105153 extend: Ext.field.Field ,
105154 xtype : 'filefield',
105155
105156
105157 /**
105158 * @event change
105159 * Fires when a file has been selected
105160 * @param {Ext.field.File} this This field
105161 * @param {Mixed} newValue The new value
105162 * @param {Mixed} oldValue The original value
105163 */
105164
105165 config : {
105166 component: {
105167 xtype : 'fileinput',
105168 fastFocus: false
105169 }
105170 },
105171
105172 proxyConfig: {
105173 name: null,
105174 value: null,
105175 files:null,
105176
105177 /**
105178 * @cfg {Boolean} multiple Allow selection of multiple files
105179 *
105180 * @accessor
105181 */
105182 multiple: false,
105183
105184 /**
105185 * @cfg {String} accept File input accept attribute documented here (http://www.w3schools.com/tags/att_input_accept.asp)
105186 * Also can be simple strings -- e.g. audio, video, image
105187 *
105188 * @accessor
105189 */
105190 accept: null,
105191 /**
105192 * @cfg {String} capture File input capture attribute. Accepts values such as "camera", "camcorder", "microphone"
105193 *
105194 * @accessor
105195 */
105196 capture: null
105197 },
105198
105199 // @private
105200 isFile: true,
105201
105202 // @private
105203 initialize: function() {
105204 var me = this;
105205
105206 me.callParent();
105207
105208 me.getComponent().on({
105209 scope: this,
105210 change : 'onChange'
105211 });
105212 },
105213
105214 onChange: function(me, value, startValue) {
105215 me.fireEvent('change', this, value, startValue);
105216 }
105217 });
105218
105219 /**
105220 * Hidden fields allow you to easily inject additional data into a {@link Ext.form.Panel form} without displaying
105221 * additional fields on the screen. This is often useful for sending dynamic or previously collected data back to the
105222 * server in the same request as the normal form submission. For example, here is how we might set up a form to send
105223 * back a hidden userId field:
105224 *
105225 * @example
105226 * var form = Ext.create('Ext.form.Panel', {
105227 * fullscreen: true,
105228 * items: [
105229 * {
105230 * xtype: 'fieldset',
105231 * title: 'Enter your name',
105232 * items: [
105233 * {
105234 * xtype: 'hiddenfield',
105235 * name: 'userId',
105236 * value: 123
105237 * },
105238 * {
105239 * xtype: 'checkboxfield',
105240 * label: 'Enable notifications',
105241 * name: 'notifications'
105242 * }
105243 * ]
105244 * }
105245 * ]
105246 * });
105247 *
105248 * In the form above we created two fields - a hidden field and a {@link Ext.field.Checkbox check box field}. Only the
105249 * check box will be visible, but both fields will be submitted. Hidden fields cannot be tabbed to - they are removed
105250 * from the tab index so when your user taps the next/previous field buttons the hidden field is skipped over.
105251 *
105252 * It's easy to read and update the value of a hidden field within a form. Using the example above, we can get a
105253 * reference to the hidden field and then set it to a new value in 2 lines of code:
105254 *
105255 * var userId = form.down('hiddenfield')[0];
105256 * userId.setValue(1234);
105257 *
105258 * For more information regarding forms and fields, please review [Using Forms in Sencha Touch Guide](../../../components/forms.html)
105259 */
105260 Ext.define('Ext.field.Hidden', {
105261 extend: Ext.field.Text ,
105262 alternateClassName: 'Ext.form.Hidden',
105263 xtype: 'hiddenfield',
105264
105265 config: {
105266 /**
105267 * @cfg
105268 * @inheritdoc
105269 */
105270 component: {
105271 xtype: 'input',
105272 type : 'hidden'
105273 },
105274
105275 /**
105276 * @cfg
105277 * @inheritdoc
105278 */
105279 ui: 'hidden',
105280
105281 /**
105282 * @cfg hidden
105283 * @hide
105284 */
105285 hidden: true,
105286
105287 /**
105288 * @cfg {Number} tabIndex
105289 * @hide
105290 */
105291 tabIndex: -1
105292 }
105293 });
105294
105295 /**
105296 * The Number field creates an HTML5 number input and is usually created inside a form. Because it creates an HTML
105297 * number input field, most browsers will show a specialized virtual keyboard for entering numbers. The Number field
105298 * only accepts numerical input and also provides additional spinner UI that increases or decreases the current value
105299 * by a configured {@link #stepValue step value}. Here's how we might use one in a form:
105300 *
105301 * @example
105302 * Ext.create('Ext.form.Panel', {
105303 * fullscreen: true,
105304 * items: [
105305 * {
105306 * xtype: 'fieldset',
105307 * title: 'How old are you?',
105308 * items: [
105309 * {
105310 * xtype: 'numberfield',
105311 * label: 'Age',
105312 * minValue: 18,
105313 * maxValue: 150,
105314 * name: 'age'
105315 * }
105316 * ]
105317 * }
105318 * ]
105319 * });
105320 *
105321 * Or on its own, outside of a form:
105322 *
105323 * Ext.create('Ext.field.Number', {
105324 * label: 'Age',
105325 * value: '26'
105326 * });
105327 *
105328 * ## minValue, maxValue and stepValue
105329 *
105330 * The {@link #minValue} and {@link #maxValue} configurations are self-explanatory and simply constrain the value
105331 * entered to the range specified by the configured min and max values. The other option exposed by this component
105332 * is {@link #stepValue}, which enables you to set how much the value changes every time the up and down spinners
105333 * are tapped on. For example, to create a salary field that ticks up and down by $1,000 each tap we can do this:
105334 *
105335 * @example
105336 * Ext.create('Ext.form.Panel', {
105337 * fullscreen: true,
105338 * items: [
105339 * {
105340 * xtype: 'fieldset',
105341 * title: 'Are you rich yet?',
105342 * items: [
105343 * {
105344 * xtype: 'numberfield',
105345 * label: 'Salary',
105346 * value: 30000,
105347 * minValue: 25000,
105348 * maxValue: 50000,
105349 * stepValue: 1000
105350 * }
105351 * ]
105352 * }
105353 * ]
105354 * });
105355 *
105356 * This creates a field that starts with a value of $30,000, steps up and down in $1,000 increments and will not go
105357 * beneath $25,000 or above $50,000.
105358 *
105359 * Because number field inherits from {@link Ext.field.Text textfield} it gains all of the functionality that text
105360 * fields provide, including getting and setting the value at runtime, validations and various events that are fired as
105361 * the user interacts with the component. Check out the {@link Ext.field.Text} docs to see the additional functionality
105362 * available.
105363 *
105364 * For more information regarding forms and fields, please review [Using Forms in Sencha Touch Guide](../../../components/forms.html)
105365 */
105366 Ext.define('Ext.field.Number', {
105367 extend: Ext.field.Text ,
105368 xtype: 'numberfield',
105369 alternateClassName: 'Ext.form.Number',
105370
105371 config: {
105372 /**
105373 * @cfg
105374 * @inheritdoc
105375 */
105376 component: {
105377 type: 'number'
105378 },
105379
105380 /**
105381 * @cfg
105382 * @inheritdoc
105383 */
105384 ui: 'number'
105385 },
105386
105387 proxyConfig: {
105388 /**
105389 * @cfg {Number} minValue The minimum value that this Number field can accept
105390 * @accessor
105391 */
105392 minValue: null,
105393
105394 /**
105395 * @cfg {Number} maxValue The maximum value that this Number field can accept
105396 * @accessor
105397 */
105398 maxValue: null,
105399
105400 /**
105401 * @cfg {Number} stepValue The amount by which the field is incremented or decremented each time the spinner is tapped.
105402 * Defaults to undefined, which means that the field goes up or down by 1 each time the spinner is tapped
105403 * @accessor
105404 */
105405 stepValue: null
105406 },
105407
105408 applyPlaceHolder: function(value) {
105409 // Android 4.1 & lower require a hack for placeholder text in number fields when using the Stock Browser
105410 // details here https://code.google.com/p/android/issues/detail?id=24626
105411 this._enableNumericPlaceHolderHack = ((!Ext.feature.has.NumericInputPlaceHolder) && (!Ext.isEmpty(value)));
105412 return value;
105413 },
105414
105415 onFocus: function(e) {
105416 if (this._enableNumericPlaceHolderHack) {
105417 this.getComponent().input.dom.setAttribute("type", "number");
105418 }
105419 this.callParent(arguments);
105420 },
105421
105422 onBlur: function(e) {
105423 if (this._enableNumericPlaceHolderHack) {
105424 this.getComponent().input.dom.setAttribute("type", "text");
105425 }
105426 this.callParent(arguments);
105427 },
105428
105429 doInitValue : function() {
105430 var value = this.getInitialConfig().value;
105431
105432 if (value) {
105433 value = this.applyValue(value);
105434 }
105435
105436 this.originalValue = value;
105437 },
105438
105439 applyValue: function(value) {
105440 var minValue = this.getMinValue(),
105441 maxValue = this.getMaxValue();
105442
105443 if (Ext.isNumber(minValue) && Ext.isNumber(value)) {
105444 value = Math.max(value, minValue);
105445 }
105446
105447 if (Ext.isNumber(maxValue) && Ext.isNumber(value)) {
105448 value = Math.min(value, maxValue);
105449 }
105450
105451 value = parseFloat(value);
105452 return (isNaN(value)) ? '' : value;
105453 },
105454
105455 getValue: function() {
105456 var value = parseFloat(this.callParent(), 10);
105457 return (isNaN(value)) ? null : value;
105458 },
105459
105460 doClearIconTap: function(me, e) {
105461 me.getComponent().setValue('');
105462 me.getValue();
105463 me.hideClearIcon();
105464 }
105465 });
105466
105467 /**
105468 * The Password field creates a password input and is usually created inside a form. Because it creates a password
105469 * field, when the user enters text it will show up as stars. Aside from that, the password field is just a normal text
105470 * field. Here's an example of how to use it in a form:
105471 *
105472 * @example
105473 * Ext.create('Ext.form.Panel', {
105474 * fullscreen: true,
105475 * items: [
105476 * {
105477 * xtype: 'fieldset',
105478 * title: 'Register',
105479 * items: [
105480 * {
105481 * xtype: 'emailfield',
105482 * label: 'Email',
105483 * name: 'email'
105484 * },
105485 * {
105486 * xtype: 'passwordfield',
105487 * label: 'Password',
105488 * name: 'password'
105489 * }
105490 * ]
105491 * }
105492 * ]
105493 * });
105494 *
105495 * Or on its own, outside of a form:
105496 *
105497 * Ext.create('Ext.field.Password', {
105498 * label: 'Password',
105499 * value: 'existingPassword'
105500 * });
105501 *
105502 * Because the password field inherits from {@link Ext.field.Text textfield} it gains all of the functionality that text
105503 * fields provide, including getting and setting the value at runtime, validations and various events that are fired as
105504 * the user interacts with the component. Check out the {@link Ext.field.Text} docs to see the additional functionality
105505 * available.
105506 *
105507 * For more information regarding forms and fields, please review [Using Forms in Sencha Touch Guide](../../../components/forms.html)
105508 */
105509 Ext.define('Ext.field.Password', {
105510 extend: Ext.field.Text ,
105511 xtype: 'passwordfield',
105512 alternateClassName: 'Ext.form.Password',
105513
105514 config: {
105515 /**
105516 * @cfg autoCapitalize
105517 * @inheritdoc
105518 */
105519 autoCapitalize: false,
105520
105521 /**
105522 * @cfg revealable {Boolean}
105523 * Enables the reveal toggle button that will show the password in clear text. This is currently only implemented in the Blackberry theme
105524 */
105525 revealable: false,
105526
105527 /**
105528 * @cfg revealed {Boolean}
105529 * A value of 'true' for this config will show the password from clear text
105530 */
105531 revealed: false,
105532
105533 /**
105534 * @cfg component
105535 * @inheritdoc
105536 */
105537 component: {
105538 type: 'password'
105539 }
105540 },
105541
105542 platformConfig: [{
105543 theme: ['Blackberry', 'Blackberry103'],
105544 revealable: true
105545 }],
105546
105547 isPassword: true,
105548
105549 initialize: function() {
105550 this.callParent(arguments);
105551 this.addCls(Ext.baseCSSPrefix + 'field-password');
105552 },
105553
105554 updateRevealable: function(newValue, oldValue) {
105555 if(newValue === oldValue) return;
105556
105557 if(this.$revealIcon) {
105558 this.getComponent().element.removeChild(this.$revealIcon);
105559 this.$revealIcon = null;
105560 }
105561
105562 if(newValue === true) {
105563 this.$revealIcon = new Ext.Element(Ext.Element.create({cls:'x-reveal-icon'}, true));
105564 this.$revealIcon.on({
105565 tap: 'onRevealIconTap',
105566 touchstart: 'onRevealIconPress',
105567 touchend: 'onRevealIconRelease',
105568 scope: this
105569 });
105570 this.getComponent().element.appendChild(this.$revealIcon);
105571 }
105572 },
105573
105574 updateRevealed: function(newValue, oldValue) {
105575 var component = this.getComponent();
105576
105577 if(newValue) {
105578 this.element.addCls(Ext.baseCSSPrefix + 'revealed');
105579 component.setType("text");
105580 } else {
105581 this.element.removeCls(Ext.baseCSSPrefix + 'revealed');
105582 component.setType("password");
105583 }
105584 },
105585
105586 // @private
105587 updateValue: function(newValue) {
105588 var component = this.getComponent(),
105589 // allows newValue to be zero but not undefined or null (other falsey values)
105590 valueValid = newValue !== undefined && newValue !== null && newValue !== "";
105591
105592 if (component) {
105593 component.setValue(newValue);
105594 }
105595
105596 this[valueValid && this.isDirty() ? 'showClearIcon' : 'hideClearIcon']();
105597
105598 this.syncEmptyCls();
105599
105600 this[valueValid ? 'showRevealIcon' : 'hideRevealIcon']();
105601 },
105602
105603 doKeyUp: function(me, e) {
105604 // getValue to ensure that we are in sync with the dom
105605 var value = me.getValue(),
105606 // allows value to be zero but not undefined or null (other falsey values)
105607 valueValid = value !== undefined && value !== null && value !== "";
105608
105609 this[valueValid ? 'showClearIcon' : 'hideClearIcon']();
105610
105611 if (e.browserEvent.keyCode === 13) {
105612 me.fireAction('action', [me, e], 'doAction');
105613 }
105614
105615 this[valueValid ? 'showRevealIcon' : 'hideRevealIcon']();
105616 },
105617
105618 // @private
105619 showRevealIcon: function() {
105620 var me = this,
105621 value = me.getValue(),
105622 // allows value to be zero but not undefined or null (other falsey values)
105623 valueValid = value !== undefined && value !== null && value !== "";
105624
105625 if (me.getRevealable() && !me.getDisabled() && valueValid) {
105626 me.element.addCls(Ext.baseCSSPrefix + 'field-revealable');
105627 }
105628
105629 return me;
105630 },
105631
105632 // @private
105633 hideRevealIcon: function() {
105634 if (this.getRevealable()) {
105635 this.element.removeCls(Ext.baseCSSPrefix + 'field-revealable');
105636 }
105637 },
105638
105639 onRevealIconTap: function(e) {
105640 this.fireAction('revealicontap', [this, e], 'doRevealIconTap');
105641 },
105642
105643 // @private
105644 doRevealIconTap: function(me, e) {
105645 if(this.getRevealed()) {
105646 this.setRevealed(false)
105647 } else {
105648 this.setRevealed(true)
105649 }
105650 },
105651
105652 onRevealIconPress: function() {
105653 this.$revealIcon.addCls(Ext.baseCSSPrefix + 'pressing');
105654 },
105655
105656 onRevealIconRelease: function() {
105657 this.$revealIcon.removeCls(Ext.baseCSSPrefix + 'pressing');
105658 }
105659 });
105660
105661 /**
105662 * The radio field is an enhanced version of the native browser radio controls and is a good way of allowing your user
105663 * to choose one option out of a selection of several (for example, choosing a favorite color):
105664 *
105665 * @example
105666 * var form = Ext.create('Ext.form.Panel', {
105667 * fullscreen: true,
105668 * items: [
105669 * {
105670 * xtype: 'radiofield',
105671 * name : 'color',
105672 * value: 'red',
105673 * label: 'Red',
105674 * checked: true
105675 * },
105676 * {
105677 * xtype: 'radiofield',
105678 * name : 'color',
105679 * value: 'green',
105680 * label: 'Green'
105681 * },
105682 * {
105683 * xtype: 'radiofield',
105684 * name : 'color',
105685 * value: 'blue',
105686 * label: 'Blue'
105687 * }
105688 * ]
105689 * });
105690 *
105691 * Above we created a simple form which allows the user to pick a color from the options red, green and blue. Because
105692 * we gave each of the fields above the same {@link #name}, the radio field ensures that only one of them can be
105693 * checked at a time. When we come to get the values out of the form again or submit it to the server, only 1 value
105694 * will be sent for each group of radio fields with the same name:
105695 *
105696 * form.getValues(); //looks like {color: 'red'}
105697 * form.submit(); //sends a single field back to the server (in this case color: red)
105698 *
105699 * For more information regarding forms and fields, please review [Using Forms in Sencha Touch Guide](../../../components/forms.html)
105700 */
105701 Ext.define('Ext.field.Radio', {
105702 extend: Ext.field.Checkbox ,
105703 xtype: 'radiofield',
105704 alternateClassName: 'Ext.form.Radio',
105705
105706 isRadio: true,
105707
105708 config: {
105709 /**
105710 * @cfg
105711 * @inheritdoc
105712 */
105713 ui: 'radio',
105714
105715 /**
105716 * @cfg
105717 * @inheritdoc
105718 */
105719 component: {
105720 type: 'radio',
105721 cls: Ext.baseCSSPrefix + 'input-radio'
105722 }
105723 },
105724
105725 getValue: function() {
105726 return (typeof this._value === 'undefined') ? null : this._value;
105727 },
105728
105729 setValue: function(value) {
105730 this._value = value;
105731 return this;
105732 },
105733
105734 getSubmitValue: function() {
105735 var value = this._value;
105736 if (typeof value == "undefined" || value == null) {
105737 value = true;
105738 }
105739 return (this.getChecked()) ? value : null;
105740 },
105741
105742 updateChecked: function(newChecked) {
105743 this.getComponent().setChecked(newChecked);
105744
105745 if (this.initialized) {
105746 this.refreshGroupValues();
105747 }
105748 },
105749
105750 // @private
105751 onMaskTap: function(component, e) {
105752 var me = this,
105753 dom = me.getComponent().input.dom;
105754
105755 if (me.getDisabled()) {
105756 return false;
105757 }
105758
105759 if (!me.getChecked()) {
105760 dom.checked = true;
105761 }
105762
105763 me.refreshGroupValues();
105764
105765 //return false so the mask does not disappear
105766 return false;
105767 },
105768
105769 /**
105770 * Returns the selected value if this radio is part of a group (other radio fields with the same name, in the same FormPanel),
105771 * @return {String}
105772 */
105773 getGroupValue: function() {
105774 var fields = this.getSameGroupFields(),
105775 ln = fields.length,
105776 i = 0,
105777 field;
105778
105779 for (; i < ln; i++) {
105780 field = fields[i];
105781 if (field.getChecked()) {
105782 return field.getValue();
105783 }
105784 }
105785
105786 return null;
105787 },
105788
105789 /**
105790 * Set the matched radio field's status (that has the same value as the given string) to checked.
105791 * @param {String} value The value of the radio field to check.
105792 * @return {Ext.field.Radio} The field that is checked.
105793 */
105794 setGroupValue: function(value) {
105795 var fields = this.getSameGroupFields(),
105796 ln = fields.length,
105797 i = 0,
105798 field;
105799
105800 for (; i < ln; i++) {
105801 field = fields[i];
105802 if (field.getValue() === value) {
105803 field.setChecked(true);
105804 return field;
105805 }
105806 }
105807 },
105808
105809 /**
105810 * Loops through each of the fields this radiofield is linked to (has the same name) and
105811 * calls `onChange` on those fields so the appropriate event is fired.
105812 * @private
105813 */
105814 refreshGroupValues: function() {
105815 var fields = this.getSameGroupFields(),
105816 ln = fields.length,
105817 i = 0,
105818 field;
105819
105820 for (; i < ln; i++) {
105821 field = fields[i];
105822 field.onChange();
105823 }
105824 }
105825 });
105826
105827 /**
105828 * The Search field creates an HTML5 search input and is usually created inside a form. Because it creates an HTML
105829 * search input type, the visual styling of this input is slightly different to normal text input controls (the corners
105830 * are rounded), though the virtual keyboard displayed by the operating system is the standard keyboard control.
105831 *
105832 * As with all other form fields in Sencha Touch, the search field gains a "clear" button that appears whenever there
105833 * is text entered into the form, and which removes that text when tapped.
105834 *
105835 * @example
105836 * Ext.create('Ext.form.Panel', {
105837 * fullscreen: true,
105838 * items: [
105839 * {
105840 * xtype: 'fieldset',
105841 * title: 'Search',
105842 * items: [
105843 * {
105844 * xtype: 'searchfield',
105845 * label: 'Query',
105846 * name: 'query'
105847 * }
105848 * ]
105849 * }
105850 * ]
105851 * });
105852 *
105853 * Or on its own, outside of a form:
105854 *
105855 * Ext.create('Ext.field.Search', {
105856 * label: 'Search:',
105857 * value: 'query'
105858 * });
105859 *
105860 * Because search field inherits from {@link Ext.field.Text textfield} it gains all of the functionality that text
105861 * fields provide, including getting and setting the value at runtime, validations and various events that are fired
105862 * as the user interacts with the component. Check out the {@link Ext.field.Text} docs to see the additional
105863 * functionality available.
105864 *
105865 * For more information regarding forms and fields, please review [Using Forms in Sencha Touch Guide](../../../components/forms.html)
105866 */
105867 Ext.define('Ext.field.Search', {
105868 extend: Ext.field.Text ,
105869 xtype: 'searchfield',
105870 alternateClassName: 'Ext.form.Search',
105871
105872 config: {
105873 /**
105874 * @cfg
105875 * @inheritdoc
105876 */
105877 component: {
105878 type: 'search'
105879 },
105880
105881 /**
105882 * @cfg
105883 * @inheritdoc
105884 */
105885 ui: 'search'
105886 },
105887
105888 platformConfig: [{
105889 platform: 'blackberry',
105890 component: {
105891 type: 'text'
105892 }
105893 }]
105894 });
105895
105896 /**
105897 * @private
105898 * Utility class used by Ext.slider.Slider - should never need to be used directly.
105899 */
105900 Ext.define('Ext.slider.Thumb', {
105901 extend: Ext.Component ,
105902 xtype : 'thumb',
105903
105904 config: {
105905 /**
105906 * @cfg
105907 * @inheritdoc
105908 */
105909 baseCls: Ext.baseCSSPrefix + 'thumb',
105910
105911 /**
105912 * @cfg {String} pressedCls
105913 * The CSS class to add to the Slider when it is pressed.
105914 * @accessor
105915 */
105916 pressedCls: Ext.baseCSSPrefix + 'thumb-pressing',
105917
105918 /**
105919 * @cfg
105920 * @inheritdoc
105921 */
105922 draggable: {
105923 direction: 'horizontal'
105924 }
105925 },
105926
105927 // Strange issue where the thumbs translation value is not being set when it is not visible. Happens when the thumb
105928 // is contained within a modal panel.
105929 platformConfig: [
105930 {
105931 platform: ['ie10'],
105932 draggable: {
105933 translatable: {
105934 translationMethod: 'csstransform'
105935 }
105936 }
105937 }
105938 ],
105939
105940 elementWidth: 0,
105941
105942 initialize: function() {
105943 this.callParent();
105944
105945 this.getDraggable().onBefore({
105946 dragstart: 'onDragStart',
105947 drag: 'onDrag',
105948 dragend: 'onDragEnd',
105949 scope: this
105950 });
105951
105952 this.getDraggable().on({
105953 touchstart: 'onPress',
105954 touchend: 'onRelease',
105955 scope: this
105956 });
105957
105958 this.element.on('resize', 'onElementResize', this);
105959 },
105960
105961 getTemplate: function() {
105962 if (Ext.theme.is.Blackberry || Ext.theme.is.Blackberry103) {
105963 return [
105964 {
105965 tag: 'div',
105966 className: Ext.baseCSSPrefix + 'thumb-inner',
105967 reference: 'innerElement'
105968 }
105969 ]
105970 } else {
105971 return this.template;
105972 }
105973 },
105974
105975
105976 /**
105977 * @private
105978 */
105979 updatePressedCls: function(pressedCls, oldPressedCls) {
105980 var element = this.element;
105981
105982 if (element.hasCls(oldPressedCls)) {
105983 element.replaceCls(oldPressedCls, pressedCls);
105984 }
105985 },
105986
105987 // @private
105988 onPress: function() {
105989 var me = this,
105990 element = me.element,
105991 pressedCls = me.getPressedCls();
105992
105993 if (!me.getDisabled()) {
105994 element.addCls(pressedCls);
105995 }
105996 },
105997
105998 // @private
105999 onRelease: function(e) {
106000 this.fireAction('release', [this, e], 'doRelease');
106001 },
106002
106003 // @private
106004 doRelease: function(me, e) {
106005 if (!me.getDisabled()) {
106006 me.element.removeCls(me.getPressedCls());
106007 }
106008 },
106009
106010 onDragStart: function() {
106011 if (this.isDisabled()) {
106012 return false;
106013 }
106014
106015 this.relayEvent(arguments);
106016 },
106017
106018 onDrag: function() {
106019 if (this.isDisabled()) {
106020 return false;
106021 }
106022
106023 this.relayEvent(arguments);
106024 },
106025
106026 onDragEnd: function() {
106027 if (this.isDisabled()) {
106028 return false;
106029 }
106030
106031 this.relayEvent(arguments);
106032 },
106033
106034 onElementResize: function(element, info) {
106035 this.elementWidth = info.width;
106036 },
106037
106038 getElementWidth: function() {
106039 return this.elementWidth;
106040 }
106041 });
106042
106043 /**
106044 * Utility class used by Ext.field.Slider.
106045 * @private
106046 */
106047 Ext.define('Ext.slider.Slider', {
106048 extend: Ext.Container ,
106049 xtype: 'slider',
106050
106051
106052
106053
106054
106055
106056 /**
106057 * @event change
106058 * Fires when the value changes
106059 * @param {Ext.slider.Slider} this
106060 * @param {Ext.slider.Thumb} thumb The thumb being changed
106061 * @param {Number} newValue The new value
106062 * @param {Number} oldValue The old value
106063 */
106064
106065 /**
106066 * @event dragstart
106067 * Fires when the slider thumb starts a drag
106068 * @param {Ext.slider.Slider} this
106069 * @param {Ext.slider.Thumb} thumb The thumb being dragged
106070 * @param {Array} value The start value
106071 * @param {Ext.EventObject} e
106072 */
106073
106074 /**
106075 * @event drag
106076 * Fires when the slider thumb starts a drag
106077 * @param {Ext.slider.Slider} this
106078 * @param {Ext.slider.Thumb} thumb The thumb being dragged
106079 * @param {Ext.EventObject} e
106080 */
106081
106082 /**
106083 * @event dragend
106084 * Fires when the slider thumb starts a drag
106085 * @param {Ext.slider.Slider} this
106086 * @param {Ext.slider.Thumb} thumb The thumb being dragged
106087 * @param {Array} value The end value
106088 * @param {Ext.EventObject} e
106089 */
106090 config: {
106091 baseCls: 'x-slider',
106092
106093 /**
106094 * @cfg {Object} thumbConfig The config object to factory {@link Ext.slider.Thumb} instances
106095 * @accessor
106096 */
106097 thumbConfig: {
106098 draggable: {
106099 translatable: {
106100 easingX: {
106101 duration: 300,
106102 type: 'ease-out'
106103 }
106104 }
106105 }
106106 },
106107
106108 /**
106109 * @cfg {Number} increment The increment by which to snap each thumb when its value changes. Any thumb movement
106110 * will be snapped to the nearest value that is a multiple of the increment (e.g. if increment is 10 and the user
106111 * tries to move the thumb to 67, it will be snapped to 70 instead)
106112 * @accessor
106113 */
106114 increment : 1,
106115
106116 /**
106117 * @cfg {Number/Number[]} value The value(s) of this slider's thumbs. If you pass
106118 * a number, it will assume you have just 1 thumb.
106119 * @accessor
106120 */
106121 value: 0,
106122
106123 /**
106124 * @cfg {Number} minValue The lowest value any thumb on this slider can be set to.
106125 * @accessor
106126 */
106127 minValue: 0,
106128
106129 /**
106130 * @cfg {Number} maxValue The highest value any thumb on this slider can be set to.
106131 * @accessor
106132 */
106133 maxValue: 100,
106134
106135 /**
106136 * @cfg {Boolean} allowThumbsOverlapping Whether or not to allow multiple thumbs to overlap each other.
106137 * Setting this to true guarantees the ability to select every possible value in between {@link #minValue}
106138 * and {@link #maxValue} that satisfies {@link #increment}
106139 * @accessor
106140 */
106141 allowThumbsOverlapping: false,
106142
106143 /**
106144 * @cfg {Boolean/Object} animation
106145 * The animation to use when moving the slider. Possible properties are:
106146 *
106147 * - duration
106148 * - easingX
106149 * - easingY
106150 *
106151 * @accessor
106152 */
106153 animation: true,
106154
106155 /**
106156 * Will make this field read only, meaning it cannot be changed with used interaction.
106157 * @cfg {Boolean} readOnly
106158 * @accessor
106159 */
106160 readOnly: false
106161 },
106162
106163 /**
106164 * @cfg {Number/Number[]} values Alias to {@link #value}
106165 */
106166
106167 elementWidth: 0,
106168
106169 offsetValueRatio: 0,
106170
106171 activeThumb: null,
106172
106173 constructor: function(config) {
106174 config = config || {};
106175
106176 if (config.hasOwnProperty('values')) {
106177 config.value = config.values;
106178 }
106179
106180 this.callParent([config]);
106181 },
106182
106183 // @private
106184 initialize: function() {
106185 var element = this.element;
106186
106187 this.callParent();
106188
106189 element.on({
106190 scope: this,
106191 tap: 'onTap',
106192 resize: 'onResize'
106193 });
106194
106195 this.on({
106196 scope: this,
106197 delegate: '> thumb',
106198 tap: 'onTap',
106199 dragstart: 'onThumbDragStart',
106200 drag: 'onThumbDrag',
106201 dragend: 'onThumbDragEnd'
106202 });
106203
106204 var thumb = this.getThumb(0);
106205 if(thumb) {
106206 thumb.on('resize', 'onThumbResize', this);
106207 }
106208 },
106209
106210 /**
106211 * @private
106212 */
106213 factoryThumb: function() {
106214 return Ext.factory(this.getThumbConfig(), Ext.slider.Thumb);
106215 },
106216
106217 /**
106218 * Returns the Thumb instances bound to this Slider
106219 * @return {Ext.slider.Thumb[]} The thumb instances
106220 */
106221 getThumbs: function() {
106222 return this.innerItems;
106223 },
106224
106225 /**
106226 * Returns the Thumb instance bound to this Slider
106227 * @param {Number} [index=0] The index of Thumb to return.
106228 * @return {Ext.slider.Thumb} The thumb instance
106229 */
106230 getThumb: function(index) {
106231 if (typeof index != 'number') {
106232 index = 0;
106233 }
106234
106235 return this.innerItems[index];
106236 },
106237
106238 refreshOffsetValueRatio: function() {
106239 var valueRange = this.getMaxValue() - this.getMinValue(),
106240 trackWidth = this.elementWidth - this.thumbWidth;
106241
106242 this.offsetValueRatio = trackWidth / valueRange;
106243 },
106244
106245 onThumbResize: function(){
106246 var thumb = this.getThumb(0);
106247 if (thumb) {
106248 this.thumbWidth = thumb.getElementWidth();
106249 }
106250 this.refresh();
106251 },
106252
106253 onResize: function(element, info) {
106254 this.elementWidth = info.width;
106255 this.refresh();
106256 },
106257
106258 refresh: function() {
106259 this.refreshValue();
106260 },
106261
106262 setActiveThumb: function(thumb) {
106263 var oldActiveThumb = this.activeThumb;
106264
106265 if (oldActiveThumb && oldActiveThumb !== thumb) {
106266 oldActiveThumb.setZIndex(null);
106267 }
106268
106269 this.activeThumb = thumb;
106270 thumb.setZIndex(2);
106271
106272 return this;
106273 },
106274
106275 onThumbDragStart: function(thumb, e) {
106276 if (e.absDeltaX <= e.absDeltaY || this.getReadOnly()) {
106277 return false;
106278 }
106279 else {
106280 e.stopPropagation();
106281 }
106282
106283 if (this.getAllowThumbsOverlapping()) {
106284 this.setActiveThumb(thumb);
106285 }
106286
106287 this.dragStartValue = this.getValue()[this.getThumbIndex(thumb)];
106288 this.fireEvent('dragstart', this, thumb, this.dragStartValue, e);
106289 },
106290
106291 onThumbDrag: function(thumb, e, offsetX) {
106292 var index = this.getThumbIndex(thumb),
106293 offsetValueRatio = this.offsetValueRatio,
106294 constrainedValue = this.constrainValue(this.getMinValue() + offsetX / offsetValueRatio);
106295
106296 e.stopPropagation();
106297
106298 this.setIndexValue(index, constrainedValue);
106299
106300 this.fireEvent('drag', this, thumb, this.getValue(), e);
106301
106302 return false;
106303 },
106304
106305 setIndexValue: function(index, value, animation) {
106306 var thumb = this.getThumb(index),
106307 values = this.getValue(),
106308 minValue = this.getMinValue(),
106309 offsetValueRatio = this.offsetValueRatio,
106310 increment = this.getIncrement(),
106311 draggable = thumb.getDraggable();
106312
106313 draggable.setOffset((value - minValue) * offsetValueRatio, null, animation);
106314
106315 values[index] = minValue + Math.round((draggable.offset.x / offsetValueRatio) / increment) * increment;
106316 },
106317
106318 onThumbDragEnd: function(thumb, e) {
106319 this.refreshThumbConstraints(thumb);
106320 var index = this.getThumbIndex(thumb),
106321 newValue = this.getValue()[index],
106322 oldValue = this.dragStartValue;
106323
106324 this.fireEvent('dragend', this, thumb, this.getValue(), e);
106325 if (oldValue !== newValue) {
106326 this.fireEvent('change', this, thumb, newValue, oldValue);
106327 }
106328 },
106329
106330 getThumbIndex: function(thumb) {
106331 return this.getThumbs().indexOf(thumb);
106332 },
106333
106334 refreshThumbConstraints: function(thumb) {
106335 var allowThumbsOverlapping = this.getAllowThumbsOverlapping(),
106336 offsetX = thumb.getDraggable().getOffset().x,
106337 thumbs = this.getThumbs(),
106338 index = this.getThumbIndex(thumb),
106339 previousThumb = thumbs[index - 1],
106340 nextThumb = thumbs[index + 1],
106341 thumbWidth = this.thumbWidth;
106342
106343 if (previousThumb) {
106344 previousThumb.getDraggable().addExtraConstraint({
106345 max: {
106346 x: offsetX - ((allowThumbsOverlapping) ? 0 : thumbWidth)
106347 }
106348 });
106349 }
106350
106351 if (nextThumb) {
106352 nextThumb.getDraggable().addExtraConstraint({
106353 min: {
106354 x: offsetX + ((allowThumbsOverlapping) ? 0 : thumbWidth)
106355 }
106356 });
106357 }
106358 },
106359
106360 // @private
106361 onTap: function(e) {
106362 if (this.isDisabled() || this.getReadOnly()) {
106363 return;
106364 }
106365
106366 var targetElement = Ext.get(e.target);
106367
106368 if (!targetElement || (Ext.browser.engineName == 'WebKit' && targetElement.hasCls('x-thumb'))) {
106369 return;
106370 }
106371
106372 var touchPointX = e.touch.point.x,
106373 element = this.element,
106374 elementX = element.getX(),
106375 offset = touchPointX - elementX - (this.thumbWidth / 2),
106376 value = this.constrainValue(this.getMinValue() + offset / this.offsetValueRatio),
106377 values = this.getValue(),
106378 minDistance = Infinity,
106379 ln = values.length,
106380 i, absDistance, testValue, closestIndex, oldValue, thumb;
106381
106382 if (ln === 1) {
106383 closestIndex = 0;
106384 }
106385 else {
106386 for (i = 0; i < ln; i++) {
106387 testValue = values[i];
106388 absDistance = Math.abs(testValue - value);
106389
106390 if (absDistance < minDistance) {
106391 minDistance = absDistance;
106392 closestIndex = i;
106393 }
106394 }
106395 }
106396
106397 oldValue = values[closestIndex];
106398 thumb = this.getThumb(closestIndex);
106399
106400 this.setIndexValue(closestIndex, value, this.getAnimation());
106401 this.refreshThumbConstraints(thumb);
106402
106403 if (oldValue !== value) {
106404 this.fireEvent('change', this, thumb, value, oldValue);
106405 }
106406 },
106407
106408 // @private
106409 updateThumbs: function(newThumbs) {
106410 this.add(newThumbs);
106411 },
106412
106413 applyValue: function(value) {
106414 var values = Ext.Array.from(value || 0),
106415 filteredValues = [],
106416 previousFilteredValue = this.getMinValue(),
106417 filteredValue, i, ln;
106418
106419 for (i = 0,ln = values.length; i < ln; i++) {
106420 filteredValue = this.constrainValue(values[i]);
106421
106422 if (filteredValue < previousFilteredValue) {
106423 //<debug warn>
106424 Ext.Logger.warn("Invalid values of '"+Ext.encode(values)+"', values at smaller indexes must " +
106425 "be smaller than or equal to values at greater indexes");
106426 //</debug>
106427 filteredValue = previousFilteredValue;
106428 }
106429
106430 filteredValues.push(filteredValue);
106431
106432 previousFilteredValue = filteredValue;
106433 }
106434
106435 return filteredValues;
106436 },
106437
106438 /**
106439 * Updates the sliders thumbs with their new value(s)
106440 */
106441 updateValue: function(newValue, oldValue) {
106442 var thumbs = this.getThumbs(),
106443 ln = newValue.length,
106444 minValue = this.getMinValue(),
106445 offset = this.offsetValueRatio,
106446 i;
106447
106448 this.setThumbsCount(ln);
106449
106450 for (i = 0; i < ln; i++) {
106451 thumbs[i].getDraggable().setExtraConstraint(null).setOffset((newValue[i] - minValue) * offset);
106452 }
106453
106454 for (i = 0; i < ln; i++) {
106455 this.refreshThumbConstraints(thumbs[i]);
106456 }
106457 },
106458
106459 /**
106460 * @private
106461 */
106462 refreshValue: function() {
106463 this.refreshOffsetValueRatio();
106464
106465 this.setValue(this.getValue());
106466 },
106467
106468 /**
106469 * @private
106470 * Takes a desired value of a thumb and returns the nearest snap value. e.g if minValue = 0, maxValue = 100, increment = 10 and we
106471 * pass a value of 67 here, the returned value will be 70. The returned number is constrained within {@link #minValue} and {@link #maxValue},
106472 * so in the above example 68 would be returned if {@link #maxValue} was set to 68.
106473 * @param {Number} value The value to snap
106474 * @return {Number} The snapped value
106475 */
106476 constrainValue: function(value) {
106477 var me = this,
106478 minValue = me.getMinValue(),
106479 maxValue = me.getMaxValue(),
106480 increment = me.getIncrement(),
106481 remainder;
106482
106483 value = parseFloat(value);
106484
106485 if (isNaN(value)) {
106486 value = minValue;
106487 }
106488
106489 remainder = (value - minValue) % increment;
106490 value -= remainder;
106491
106492 if (Math.abs(remainder) >= (increment / 2)) {
106493 value += (remainder > 0) ? increment : -increment;
106494 }
106495
106496 value = Math.max(minValue, value);
106497 value = Math.min(maxValue, value);
106498
106499 return value;
106500 },
106501
106502 setThumbsCount: function(count) {
106503 var thumbs = this.getThumbs(),
106504 thumbsCount = thumbs.length,
106505 i, ln, thumb;
106506
106507 if (thumbsCount > count) {
106508 for (i = 0,ln = thumbsCount - count; i < ln; i++) {
106509 thumb = thumbs[thumbs.length - 1];
106510 thumb.destroy();
106511 }
106512 }
106513 else if (thumbsCount < count) {
106514 for (i = 0,ln = count - thumbsCount; i < ln; i++) {
106515 this.add(this.factoryThumb());
106516 }
106517 }
106518
106519 return this;
106520 },
106521
106522 /**
106523 * Convenience method. Calls {@link #setValue}.
106524 */
106525 setValues: function(value) {
106526 this.setValue(value);
106527 },
106528
106529 /**
106530 * Convenience method. Calls {@link #getValue}.
106531 * @return {Object}
106532 */
106533 getValues: function() {
106534 return this.getValue();
106535 },
106536
106537 /**
106538 * Sets the {@link #increment} configuration.
106539 * @param {Number} increment
106540 * @return {Number}
106541 */
106542 applyIncrement: function(increment) {
106543 if (increment === 0) {
106544 increment = 1;
106545 }
106546
106547 return Math.abs(increment);
106548 },
106549
106550 // @private
106551 updateAllowThumbsOverlapping: function(newValue, oldValue) {
106552 if (typeof oldValue != 'undefined') {
106553 this.refreshValue();
106554 }
106555 },
106556
106557 // @private
106558 updateMinValue: function(newValue, oldValue) {
106559 if (typeof oldValue != 'undefined') {
106560 this.refreshValue();
106561 }
106562 },
106563
106564 // @private
106565 updateMaxValue: function(newValue, oldValue) {
106566 if (typeof oldValue != 'undefined') {
106567 this.refreshValue();
106568 }
106569 },
106570
106571 // @private
106572 updateIncrement: function(newValue, oldValue) {
106573 if (typeof oldValue != 'undefined') {
106574 this.refreshValue();
106575 }
106576 },
106577
106578 doSetDisabled: function(disabled) {
106579 this.callParent(arguments);
106580
106581 var items = this.getItems().items,
106582 ln = items.length,
106583 i;
106584
106585 for (i = 0; i < ln; i++) {
106586 items[i].setDisabled(disabled);
106587 }
106588 }
106589
106590 }, function() {
106591 });
106592
106593 /**
106594 * The slider is a way to allow the user to select a value from a given numerical range. You might use it for choosing
106595 * a percentage, combine two of them to get min and max values, or use three of them to specify the hex values for a
106596 * color. Each slider contains a single 'thumb' that can be dragged along the slider's length to change the value.
106597 * Sliders are equally useful inside {@link Ext.form.Panel forms} and standalone. Here's how to quickly create a
106598 * slider in form, in this case enabling a user to choose a percentage:
106599 *
106600 * @example
106601 * Ext.create('Ext.form.Panel', {
106602 * fullscreen: true,
106603 * items: [
106604 * {
106605 * xtype: 'sliderfield',
106606 * label: 'Percentage',
106607 * value: 50,
106608 * minValue: 0,
106609 * maxValue: 100
106610 * }
106611 * ]
106612 * });
106613 *
106614 * In this case we set a starting value of 50%, and defined the min and max values to be 0 and 100 respectively, giving
106615 * us a percentage slider. Because this is such a common use case, the defaults for {@link #minValue} and
106616 * {@link #maxValue} are already set to 0 and 100 so in the example above they could be removed.
106617 *
106618 * It's often useful to render sliders outside the context of a form panel too. In this example we create a slider that
106619 * allows a user to choose the waist measurement of a pair of jeans. Let's say the online store we're making this for
106620 * sells jeans with waist sizes from 24 inches to 60 inches in 2 inch increments - here's how we might achieve that:
106621 *
106622 * @example
106623 * Ext.create('Ext.form.Panel', {
106624 * fullscreen: true,
106625 * items: [
106626 * {
106627 * xtype: 'sliderfield',
106628 * label: 'Waist Measurement',
106629 * minValue: 24,
106630 * maxValue: 60,
106631 * increment: 2,
106632 * value: 32
106633 * }
106634 * ]
106635 * });
106636 *
106637 * Now that we've got our slider, we can ask it what value it currently has and listen to events that it fires. For
106638 * example, if we wanted our app to show different images for different sizes, we can listen to the {@link #change}
106639 * event to be informed whenever the slider is moved:
106640 *
106641 * slider.on('change', function(field, newValue) {
106642 * if (newValue[0] > 40) {
106643 * imgComponent.setSrc('large.png');
106644 * } else {
106645 * imgComponent.setSrc('small.png');
106646 * }
106647 * }, this);
106648 *
106649 * Here we listened to the {@link #change} event on the slider and updated the background image of an
106650 * {@link Ext.Img image component} based on what size the user selected. Of course, you can use any logic inside your
106651 * event listener.
106652 *
106653 * For more information regarding forms and fields, please review [Using Forms in Sencha Touch Guide](../../../components/forms.html)
106654 */
106655 Ext.define('Ext.field.Slider', {
106656 extend : Ext.field.Field ,
106657 xtype : 'sliderfield',
106658
106659 alternateClassName: 'Ext.form.Slider',
106660
106661 /**
106662 * @event change
106663 * Fires when an option selection has changed.
106664 * @param {Ext.field.Slider} me
106665 * @param {Ext.slider.Slider} sl Slider Component.
106666 * @param {Ext.slider.Thumb} thumb
106667 * @param {Number} newValue The new value of this thumb.
106668 * @param {Number} oldValue The old value of this thumb.
106669 */
106670
106671 /**
106672 * @event dragstart
106673 * Fires when the slider thumb starts a drag operation.
106674 * @param {Ext.field.Slider} this
106675 * @param {Ext.slider.Slider} sl Slider Component.
106676 * @param {Ext.slider.Thumb} thumb The thumb being dragged.
106677 * @param {Array} value The start value.
106678 * @param {Ext.EventObject} e
106679 */
106680
106681 /**
106682 * @event drag
106683 * Fires when the slider thumb starts a drag operation.
106684 * @param {Ext.field.Slider} this
106685 * @param {Ext.slider.Slider} sl Slider Component.
106686 * @param {Ext.slider.Thumb} thumb The thumb being dragged.
106687 * @param {Ext.EventObject} e
106688 */
106689
106690 /**
106691 * @event dragend
106692 * Fires when the slider thumb ends a drag operation.
106693 * @param {Ext.field.Slider} this
106694 * @param {Ext.slider.Slider} sl Slider Component.
106695 * @param {Ext.slider.Thumb} thumb The thumb being dragged.
106696 * @param {Array} value The end value.
106697 * @param {Ext.EventObject} e
106698 */
106699
106700 config: {
106701 /**
106702 * @cfg
106703 * @inheritdoc
106704 */
106705 cls: Ext.baseCSSPrefix + 'slider-field',
106706
106707 /**
106708 * @cfg
106709 * @inheritdoc
106710 */
106711 tabIndex: -1,
106712
106713 /**
106714 * Will make this field read only, meaning it cannot be changed with used interaction.
106715 * @cfg {Boolean} readOnly
106716 * @accessor
106717 */
106718 readOnly: false
106719 },
106720
106721 proxyConfig: {
106722
106723 /**
106724 * @inheritdoc Ext.slider.Slider#increment
106725 * @cfg {Number} increment
106726 * @accessor
106727 */
106728 increment : 1,
106729
106730 /**
106731 * @inheritdoc Ext.slider.Slider#value
106732 * @cfg {Number/Number[]} value
106733 * @accessor
106734 */
106735 value: 0,
106736
106737 /**
106738 * @inheritdoc Ext.slider.Slider#minValue
106739 * @cfg {Number} minValue
106740 * @accessor
106741 */
106742 minValue: 0,
106743
106744 /**
106745 * @inheritdoc Ext.slider.Slider#maxValue
106746 * @cfg {Number} maxValue
106747 * @accessor
106748 */
106749 maxValue: 100
106750 },
106751
106752 /**
106753 * @inheritdoc Ext.slider.Slider#values
106754 * @cfg {Number/Number[]} values
106755 */
106756
106757 constructor: function(config) {
106758 config = config || {};
106759
106760 if (config.hasOwnProperty('values')) {
106761 config.value = config.values;
106762 }
106763
106764 this.callParent([config]);
106765 this.updateMultipleState();
106766 },
106767
106768 // @private
106769 initialize: function() {
106770 this.callParent();
106771
106772 this.getComponent().on({
106773 scope: this,
106774
106775 change: 'onSliderChange',
106776 dragstart: 'onSliderDragStart',
106777 drag: 'onSliderDrag',
106778 dragend: 'onSliderDragEnd'
106779 });
106780 },
106781
106782 // @private
106783 applyComponent: function(config) {
106784 return Ext.factory(config, Ext.slider.Slider);
106785 },
106786
106787 // @private
106788 updateComponent: function(component) {
106789 this.callSuper(arguments);
106790
106791 component.setMinValue(this.getMinValue());
106792 component.setMaxValue(this.getMaxValue());
106793 },
106794
106795 onSliderChange: function() {
106796 this.fireEvent.apply(this, [].concat('change', this, Array.prototype.slice.call(arguments)));
106797 },
106798
106799 onSliderDragStart: function() {
106800 this.fireEvent.apply(this, [].concat('dragstart', this, Array.prototype.slice.call(arguments)));
106801 },
106802
106803 onSliderDrag: function() {
106804 this.fireEvent.apply(this, [].concat('drag', this, Array.prototype.slice.call(arguments)));
106805 },
106806
106807 onSliderDragEnd: function() {
106808 this.fireEvent.apply(this, [].concat('dragend', this, Array.prototype.slice.call(arguments)));
106809 },
106810
106811 /**
106812 * Convenience method. Calls {@link #setValue}.
106813 * @param {Object} value
106814 */
106815 setValues: function(value) {
106816 this.setValue(value);
106817 this.updateMultipleState();
106818 },
106819
106820 /**
106821 * Convenience method. Calls {@link #getValue}
106822 * @return {Object}
106823 */
106824 getValues: function() {
106825 return this.getValue();
106826 },
106827
106828 reset: function() {
106829 var config = this.config,
106830 initialValue = (this.config.hasOwnProperty('values')) ? config.values : config.value;
106831
106832 this.setValue(initialValue);
106833 },
106834
106835 doSetDisabled: function(disabled) {
106836 this.callParent(arguments);
106837
106838 this.getComponent().setDisabled(disabled);
106839 },
106840
106841 updateReadOnly: function(newValue) {
106842 this.getComponent().setReadOnly(newValue);
106843 },
106844
106845 isDirty : function () {
106846 if (this.getDisabled()) {
106847 return false;
106848 }
106849
106850 return this.getValue() !== this.originalValue;
106851 },
106852
106853 updateMultipleState: function() {
106854 var value = this.getValue();
106855 if (value && value.length > 1) {
106856 this.addCls(Ext.baseCSSPrefix + 'slider-multiple');
106857 }
106858 }
106859 });
106860
106861 /**
106862 * A wrapper class which can be applied to any element. Fires a "tap" event while
106863 * touching the device. The interval between firings may be specified in the config but
106864 * defaults to 20 milliseconds.
106865 */
106866 Ext.define('Ext.util.TapRepeater', {
106867
106868
106869 mixins: {
106870 observable: Ext.mixin.Observable
106871 },
106872
106873 /**
106874 * @event touchstart
106875 * Fires when the touch is started.
106876 * @param {Ext.util.TapRepeater} this
106877 * @param {Ext.event.Event} e
106878 */
106879
106880 /**
106881 * @event tap
106882 * Fires on a specified interval during the time the element is pressed.
106883 * @param {Ext.util.TapRepeater} this
106884 * @param {Ext.event.Event} e
106885 */
106886
106887 /**
106888 * @event touchend
106889 * Fires when the touch is ended.
106890 * @param {Ext.util.TapRepeater} this
106891 * @param {Ext.event.Event} e
106892 */
106893
106894 config: {
106895 el: null,
106896 accelerate: true,
106897 interval: 10,
106898 delay: 250,
106899 preventDefault: true,
106900 stopDefault: false,
106901 timer: 0,
106902 pressCls: null
106903 },
106904
106905 /**
106906 * Creates new TapRepeater.
106907 * @param {Object} config
106908 */
106909 constructor: function(config) {
106910 var me = this;
106911 //<debug warn>
106912 for (var configName in config) {
106913 if (me.self.prototype.config && !(configName in me.self.prototype.config)) {
106914 me[configName] = config[configName];
106915 Ext.Logger.warn('Applied config as instance property: "' + configName + '"', me);
106916 }
106917 }
106918 //</debug>
106919 me.initConfig(config);
106920 },
106921
106922 updateEl: function(newEl, oldEl) {
106923 var eventCfg = {
106924 touchstart: 'onTouchStart',
106925 touchend: 'onTouchEnd',
106926 tap: 'eventOptions',
106927 scope: this
106928 };
106929 if (oldEl) {
106930 oldEl.un(eventCfg)
106931 }
106932 newEl.on(eventCfg);
106933 },
106934
106935 // @private
106936 eventOptions: function(e) {
106937 if (this.getPreventDefault()) {
106938 e.preventDefault();
106939 }
106940 if (this.getStopDefault()) {
106941 e.stopEvent();
106942 }
106943 },
106944
106945 // @private
106946 destroy: function() {
106947 this.clearListeners();
106948 Ext.destroy(this.el);
106949 },
106950
106951 // @private
106952 onTouchStart: function(e) {
106953 var me = this,
106954 pressCls = me.getPressCls();
106955 clearTimeout(me.getTimer());
106956 if (pressCls) {
106957 me.getEl().addCls(pressCls);
106958 }
106959 me.tapStartTime = new Date();
106960
106961 me.fireEvent('touchstart', me, e);
106962 me.fireEvent('tap', me, e);
106963
106964 // Do not honor delay or interval if acceleration wanted.
106965 if (me.getAccelerate()) {
106966 me.delay = 400;
106967 }
106968 me.setTimer(Ext.defer(me.tap, me.getDelay() || me.getInterval(), me, [e]));
106969 },
106970
106971 // @private
106972 tap: function(e) {
106973 var me = this;
106974 me.fireEvent('tap', me, e);
106975 me.setTimer(Ext.defer(me.tap, me.getAccelerate() ? me.easeOutExpo(Ext.Date.getElapsed(me.tapStartTime),
106976 400,
106977 -390,
106978 12000) : me.getInterval(), me, [e]));
106979 },
106980
106981 // Easing calculation
106982 // @private
106983 easeOutExpo: function(t, b, c, d) {
106984 return (t == d) ? b + c : c * ( - Math.pow(2, -10 * t / d) + 1) + b;
106985 },
106986
106987 // @private
106988 onTouchEnd: function(e) {
106989 var me = this;
106990 clearTimeout(me.getTimer());
106991 me.getEl().removeCls(me.getPressCls());
106992 me.fireEvent('touchend', me, e);
106993 }
106994 });
106995
106996 /**
106997 * Wraps an HTML5 number field. Example usage:
106998 *
106999 * @example miniphone
107000 * var spinner = Ext.create('Ext.field.Spinner', {
107001 * label : 'Spinner Field',
107002 * minValue : 0,
107003 * maxValue : 100,
107004 * stepValue : 2,
107005 * cycle : true
107006 * });
107007 * Ext.Viewport.add({ xtype: 'container', items: [spinner] });
107008 *
107009 * For more information regarding forms and fields, please review [Using Forms in Sencha Touch Guide](../../../components/forms.html)
107010 */
107011 Ext.define('Ext.field.Spinner', {
107012 extend: Ext.field.Number ,
107013 xtype: 'spinnerfield',
107014 alternateClassName: 'Ext.form.Spinner',
107015
107016
107017 /**
107018 * @event spin
107019 * Fires when the value is changed via either spinner buttons.
107020 * @param {Ext.field.Spinner} this
107021 * @param {Number} value
107022 * @param {String} direction 'up' or 'down'.
107023 */
107024
107025 /**
107026 * @event spindown
107027 * Fires when the value is changed via the spinner down button.
107028 * @param {Ext.field.Spinner} this
107029 * @param {Number} value
107030 */
107031
107032 /**
107033 * @event spinup
107034 * Fires when the value is changed via the spinner up button.
107035 * @param {Ext.field.Spinner} this
107036 * @param {Number} value
107037 */
107038
107039 /**
107040 * @event change
107041 * Fires just before the field blurs if the field value has changed.
107042 * @param {Ext.field.Text} this This field.
107043 * @param {Number} newValue The new value.
107044 * @param {Number} oldValue The original value.
107045 */
107046
107047 /**
107048 * @event updatedata
107049 * @hide
107050 */
107051
107052 /**
107053 * @event action
107054 * @hide
107055 */
107056
107057 config: {
107058 /**
107059 * @cfg
107060 * @inheritdoc
107061 */
107062 cls: Ext.baseCSSPrefix + 'spinner',
107063
107064 /**
107065 * @cfg {Number} [minValue=-infinity] The minimum allowed value.
107066 * @accessor
107067 */
107068 minValue: Number.NEGATIVE_INFINITY,
107069
107070 /**
107071 * @cfg {Number} [maxValue=infinity] The maximum allowed value.
107072 * @accessor
107073 */
107074 maxValue: Number.MAX_VALUE,
107075
107076 /**
107077 * @cfg {Number} stepValue Value that is added or subtracted from the current value when a spinner is used.
107078 * @accessor
107079 */
107080 stepValue: 0.1,
107081
107082 /**
107083 * @cfg {Boolean} accelerateOnTapHold True if autorepeating should start slowly and accelerate.
107084 * @accessor
107085 */
107086 accelerateOnTapHold: true,
107087
107088 /**
107089 * @cfg {Boolean} cycle When set to `true`, it will loop the values of a minimum or maximum is reached.
107090 * If the maximum value is reached, the value will be set to the minimum.
107091 * @accessor
107092 */
107093 cycle: false,
107094
107095 /**
107096 * @cfg {Boolean} clearIcon
107097 * @hide
107098 * @accessor
107099 */
107100 clearIcon: false,
107101
107102 /**
107103 * @cfg {Number} defaultValue The default value for this field when no value has been set.
107104 * It is also used when the value is set to `NaN`.
107105 */
107106 defaultValue: 0,
107107
107108 /**
107109 * @cfg {Number} tabIndex
107110 * @hide
107111 */
107112 tabIndex: -1,
107113
107114 /**
107115 * @cfg {Boolean} groupButtons
107116 * `true` if you want to group the buttons to the right of the fields. `false` if you want the buttons
107117 * to be at either side of the field.
107118 */
107119 groupButtons: true,
107120
107121 /**
107122 * @cfg component
107123 * @inheritdoc
107124 */
107125 component: {
107126 disabled: true
107127 }
107128 },
107129
107130 platformConfig: [{
107131 platform: 'android',
107132 component: {
107133 disabled: false,
107134 readOnly: true
107135 }
107136 }],
107137
107138 constructor: function() {
107139 var me = this;
107140
107141 me.callParent(arguments);
107142
107143 if (!me.getValue()) {
107144 me.suspendEvents();
107145 me.setValue(me.getDefaultValue());
107146 me.resumeEvents();
107147 }
107148 },
107149
107150 syncEmptyCls: Ext.emptyFn,
107151
107152 /**
107153 * Updates the {@link #component} configuration
107154 */
107155 updateComponent: function(newComponent) {
107156 this.callParent(arguments);
107157
107158 var cls = this.getCls();
107159
107160 if (newComponent) {
107161 this.spinDownButton = Ext.Element.create({
107162 cls : cls + '-button ' + cls + '-button-down',
107163 html: '-'
107164 });
107165
107166 this.spinUpButton = Ext.Element.create({
107167 cls : cls + '-button ' + cls + '-button-up',
107168 html: '+'
107169 });
107170
107171 this.downRepeater = this.createRepeater(this.spinDownButton, this.onSpinDown);
107172 this.upRepeater = this.createRepeater(this.spinUpButton, this.onSpinUp);
107173 }
107174 },
107175
107176 updateGroupButtons: function(newGroupButtons, oldGroupButtons) {
107177 var me = this,
107178 innerElement = me.innerElement,
107179 cls = me.getBaseCls() + '-grouped-buttons';
107180
107181 me.getComponent();
107182
107183 if (newGroupButtons != oldGroupButtons) {
107184 if (newGroupButtons) {
107185 this.addCls(cls);
107186 innerElement.appendChild(me.spinDownButton);
107187 innerElement.appendChild(me.spinUpButton);
107188 } else {
107189 this.removeCls(cls);
107190 innerElement.insertFirst(me.spinDownButton);
107191 innerElement.appendChild(me.spinUpButton);
107192 }
107193 }
107194 },
107195
107196 applyValue: function(value) {
107197 value = parseFloat(value);
107198 if (isNaN(value) || value === null) {
107199 value = this.getDefaultValue();
107200 }
107201
107202 //round the value to 1 decimal
107203 value = Math.round(value * 10) / 10;
107204
107205 return this.callParent([value]);
107206 },
107207
107208 // @private
107209 createRepeater: function(el, fn) {
107210 var me = this,
107211 repeater = Ext.create('Ext.util.TapRepeater', {
107212 el: el,
107213 accelerate: me.getAccelerateOnTapHold()
107214 });
107215
107216 repeater.on({
107217 tap: fn,
107218 touchstart: 'onTouchStart',
107219 touchend: 'onTouchEnd',
107220 scope: me
107221 });
107222
107223 return repeater;
107224 },
107225
107226 // @private
107227 onSpinDown: function() {
107228 if (!this.getDisabled() && !this.getReadOnly()) {
107229 this.spin(true);
107230 }
107231 },
107232
107233 // @private
107234 onSpinUp: function() {
107235 if (!this.getDisabled() && !this.getReadOnly()) {
107236 this.spin(false);
107237 }
107238 },
107239
107240 // @private
107241 onTouchStart: function(repeater) {
107242 if (!this.getDisabled() && !this.getReadOnly()) {
107243 repeater.getEl().addCls(Ext.baseCSSPrefix + 'button-pressed');
107244 }
107245 },
107246
107247 // @private
107248 onTouchEnd: function(repeater) {
107249 repeater.getEl().removeCls(Ext.baseCSSPrefix + 'button-pressed');
107250 },
107251
107252 // @private
107253 spin: function(down) {
107254 var me = this,
107255 originalValue = me.getValue(),
107256 stepValue = me.getStepValue(),
107257 direction = down ? 'down' : 'up',
107258 minValue = me.getMinValue(),
107259 maxValue = me.getMaxValue(),
107260 value;
107261
107262 if (down) {
107263 value = originalValue - stepValue;
107264 }
107265 else {
107266 value = originalValue + stepValue;
107267 }
107268
107269 //if cycle is true, then we need to check fi the value hasn't changed and we cycle the value
107270 if (me.getCycle()) {
107271 if (originalValue == minValue && value < minValue) {
107272 value = maxValue;
107273 }
107274
107275 if (originalValue == maxValue && value > maxValue) {
107276 value = minValue;
107277 }
107278 }
107279
107280 me.setValue(value);
107281 value = me.getValue();
107282
107283 me.fireEvent('spin', me, value, direction);
107284 me.fireEvent('spin' + direction, me, value);
107285 },
107286
107287 /**
107288 * @private
107289 */
107290 doSetDisabled: function(disabled) {
107291 Ext.Component.prototype.doSetDisabled.apply(this, arguments);
107292 },
107293
107294 /**
107295 * @private
107296 */
107297 setDisabled: function() {
107298 Ext.Component.prototype.setDisabled.apply(this, arguments);
107299 },
107300
107301 reset: function() {
107302 this.setValue(this.getDefaultValue());
107303 },
107304
107305 // setValue: function(value){
107306 // this.callSuper(arguments);
107307
107308 // @TODO: Isn't this already done by the framework by default?
107309 // if(Ext.getThemeName() == 'WP'){
107310 // this.getComponent().element.dom.setAttribute('value',value);
107311 // }
107312 // },
107313
107314 // @private
107315 destroy: function() {
107316 var me = this;
107317 Ext.destroy(me.downRepeater, me.upRepeater, me.spinDownButton, me.spinUpButton);
107318 me.callParent(arguments);
107319 }
107320 }, function() {
107321 });
107322
107323 /**
107324 * @private
107325 */
107326 Ext.define('Ext.slider.Toggle', {
107327 extend: Ext.slider.Slider ,
107328
107329 config: {
107330 /**
107331 * @cfg
107332 * @inheritdoc
107333 */
107334 baseCls: 'x-toggle',
107335
107336 /**
107337 * @cfg {String} minValueCls CSS class added to the field when toggled to its minValue
107338 * @accessor
107339 */
107340 minValueCls: 'x-toggle-off',
107341
107342 /**
107343 * @cfg {String} maxValueCls CSS class added to the field when toggled to its maxValue
107344 * @accessor
107345 */
107346 maxValueCls: 'x-toggle-on'
107347 },
107348
107349 initialize: function() {
107350 this.callParent();
107351
107352 this.on({
107353 change: 'onChange'
107354 });
107355 },
107356
107357 applyMinValue: function() {
107358 return 0;
107359 },
107360
107361 applyMaxValue: function() {
107362 return 1;
107363 },
107364
107365 applyIncrement: function() {
107366 return 1;
107367 },
107368
107369 updateMinValueCls: function(newCls, oldCls) {
107370 var element = this.element;
107371
107372 if (oldCls && element.hasCls(oldCls)) {
107373 element.replaceCls(oldCls, newCls);
107374 }
107375 },
107376
107377 updateMaxValueCls: function(newCls, oldCls) {
107378 var element = this.element;
107379
107380 if (oldCls && element.hasCls(oldCls)) {
107381 element.replaceCls(oldCls, newCls);
107382 }
107383 },
107384
107385 setValue: function(newValue, oldValue) {
107386 this.callParent(arguments);
107387 this.onChange(this, this.getThumbs()[0], newValue, oldValue);
107388 },
107389
107390 setIndexValue: function(index, value, animation) {
107391 var oldValue = this.getValue()[index];
107392 this.callParent(arguments);
107393
107394 var thumb = this.getThumb(index),
107395 newValue = this.getValue()[index];
107396
107397 if (oldValue !== newValue) {
107398 this.fireEvent('change', this, thumb, newValue, oldValue);
107399 }
107400 },
107401
107402 onChange: function(me, thumb, newValue, oldValue) {
107403 var isOn = newValue > 0,
107404 onCls = me.getMaxValueCls(),
107405 offCls = me.getMinValueCls(),
107406 element = this.element;
107407
107408 element.addCls(isOn ? onCls : offCls);
107409 element.removeCls(isOn ? offCls : onCls);
107410 },
107411
107412 toggle: function() {
107413 var value = this.getValue();
107414 this.setValue((value == 1) ? 0 : 1);
107415
107416 return this;
107417 },
107418
107419 onTap: function() {
107420 if (this.isDisabled() || this.getReadOnly()) {
107421 return;
107422 }
107423
107424 var oldValue = this.getValue(),
107425 newValue = (oldValue == 1) ? 0 : 1,
107426 thumb = this.getThumb(0);
107427
107428 this.setIndexValue(0, newValue, this.getAnimation());
107429 this.refreshThumbConstraints(thumb);
107430 }
107431 });
107432
107433 /**
107434 * Specialized {@link Ext.field.Slider} with a single thumb which only supports two {@link #value values}.
107435 *
107436 * ## Examples
107437 *
107438 * @example miniphone preview
107439 * Ext.Viewport.add({
107440 * xtype: 'togglefield',
107441 * name: 'awesome',
107442 * label: 'Are you awesome?',
107443 * labelWidth: '40%'
107444 * });
107445 *
107446 * Having a default value of 'toggled':
107447 *
107448 * @example miniphone preview
107449 * Ext.Viewport.add({
107450 * xtype: 'togglefield',
107451 * name: 'awesome',
107452 * value: 1,
107453 * label: 'Are you awesome?',
107454 * labelWidth: '40%'
107455 * });
107456 *
107457 * And using the {@link #value} {@link #toggle} method:
107458 *
107459 * @example miniphone preview
107460 * Ext.Viewport.add([
107461 * {
107462 * xtype: 'togglefield',
107463 * name: 'awesome',
107464 * value: 1,
107465 * label: 'Are you awesome?',
107466 * labelWidth: '40%'
107467 * },
107468 * {
107469 * xtype: 'toolbar',
107470 * docked: 'top',
107471 * items: [
107472 * {
107473 * xtype: 'button',
107474 * text: 'Toggle',
107475 * flex: 1,
107476 * handler: function() {
107477 * Ext.ComponentQuery.query('togglefield')[0].toggle();
107478 * }
107479 * }
107480 * ]
107481 * }
107482 * ]);
107483 *
107484 * For more information regarding forms and fields, please review [Using Forms in Sencha Touch Guide](../../../components/forms.html)
107485 */
107486 Ext.define('Ext.field.Toggle', {
107487 extend: Ext.field.Slider ,
107488 xtype : 'togglefield',
107489 alternateClassName: 'Ext.form.Toggle',
107490
107491
107492 config: {
107493 /**
107494 * @cfg
107495 * @inheritdoc
107496 */
107497 cls: 'x-toggle-field',
107498
107499 /* @cfg {String} labelAlign The position to render the label relative to the field input.
107500 * Available options are: 'top', 'left', 'bottom' and 'right'
107501 * @accessor
107502 */
107503 labelAlign: 'left',
107504
107505 /**
107506 * @cfg {String} activeLabel The label to add to the toggle field when it is toggled on.
107507 * Only available in the Blackberry theme.
107508 * @accessor
107509 */
107510 activeLabel: null,
107511
107512 /**
107513 * @cfg {String} inactiveLabel The label to add to the toggle field when it is toggled off.
107514 * Only available in the Blackberry theme.
107515 * @accessor
107516 */
107517 inactiveLabel: null
107518 },
107519
107520 platformConfig: [{
107521 theme: ['Windows'],
107522 labelAlign: 'left'
107523 }, {
107524 theme: ['Blackberry', 'Blackberry103', 'MountainView'],
107525 activeLabel: 'On',
107526 inactiveLabel: 'Off'
107527 }],
107528
107529 /**
107530 * @event change
107531 * Fires when an option selection has changed.
107532 *
107533 * Ext.Viewport.add({
107534 * xtype: 'togglefield',
107535 * label: 'Event Example',
107536 * listeners: {
107537 * change: function(field, newValue, oldValue) {
107538 * console.log('Value of this toggle has changed:', (newValue) ? 'ON' : 'OFF');
107539 * }
107540 * }
107541 * });
107542 *
107543 * @param {Ext.field.Toggle} this
107544 * @param {Number} newValue the new value of this thumb
107545 * @param {Number} oldValue the old value of this thumb
107546 */
107547
107548 /**
107549 * @event dragstart
107550 * @hide
107551 */
107552
107553 /**
107554 * @event drag
107555 * @hide
107556 */
107557
107558 /**
107559 * @event dragend
107560 * @hide
107561 */
107562
107563 proxyConfig: {
107564 /**
107565 * @cfg {String} minValueCls See {@link Ext.slider.Toggle#minValueCls}
107566 * @accessor
107567 */
107568 minValueCls: 'x-toggle-off',
107569
107570 /**
107571 * @cfg {String} maxValueCls See {@link Ext.slider.Toggle#maxValueCls}
107572 * @accessor
107573 */
107574 maxValueCls: 'x-toggle-on'
107575 },
107576
107577 // @private
107578 applyComponent: function(config) {
107579 return Ext.factory(config, Ext.slider.Toggle);
107580 },
107581
107582 // @private
107583 updateActiveLabel: function(newActiveLabel, oldActiveLabel) {
107584 if (newActiveLabel != oldActiveLabel) {
107585 this.getComponent().element.dom.setAttribute('data-activelabel', newActiveLabel);
107586 }
107587 },
107588
107589 // @private
107590 updateInactiveLabel: function(newInactiveLabel, oldInactiveLabel) {
107591 if (newInactiveLabel != oldInactiveLabel) {
107592 this.getComponent().element.dom.setAttribute('data-inactivelabel', newInactiveLabel);
107593 }
107594 },
107595
107596 /**
107597 * Sets the value of the toggle.
107598 * @param {Number} newValue **1** for toggled, **0** for untoggled.
107599 * @return {Object} this
107600 */
107601 setValue: function(newValue) {
107602 if (newValue === true) {
107603 newValue = 1;
107604 }
107605
107606 var oldValue = this.getValue();
107607 if (oldValue != newValue) {
107608 this.getComponent().setValue(newValue);
107609
107610 this.fireEvent('change', this, newValue, oldValue);
107611 }
107612
107613 return this;
107614 },
107615
107616 getValue: function() {
107617 return (this.getComponent().getValue() == 1) ? 1 : 0;
107618 },
107619
107620 onSliderChange: function(component, thumb, newValue, oldValue) {
107621 this.fireEvent.call(this, 'change', this, newValue, oldValue);
107622 },
107623
107624 /**
107625 * Toggles the value of this toggle field.
107626 * @return {Object} this
107627 */
107628 toggle: function() {
107629 // We call setValue directly so the change event can be fired
107630 var value = this.getValue();
107631 this.setValue((value == 1) ? 0 : 1);
107632
107633 return this;
107634 },
107635
107636 onChange: function(){
107637 this.setLabel((this.getValue() == 1) ? this.toggleOnLabel : this.toggleOffLabel);
107638 }
107639 });
107640
107641 /**
107642 * The urlfield creates an HTML5 url input and is usually created inside a form. Because it creates an HTML url input
107643 * field, most browsers will show a specialized virtual keyboard for web address input. Aside from that, the urlfield
107644 * is just a normal text field. Here's an example of how to use it in a form:
107645 *
107646 * @example
107647 * Ext.create('Ext.form.Panel', {
107648 * fullscreen: true,
107649 * items: [
107650 * {
107651 * xtype: 'fieldset',
107652 * title: 'Add Bookmark',
107653 * items: [
107654 * {
107655 * xtype: 'urlfield',
107656 * label: 'Url',
107657 * name: 'url'
107658 * }
107659 * ]
107660 * }
107661 * ]
107662 * });
107663 *
107664 * Or on its own, outside of a form:
107665 *
107666 * Ext.create('Ext.field.Url', {
107667 * label: 'Web address',
107668 * value: 'http://sencha.com'
107669 * });
107670 *
107671 * Because url field inherits from {@link Ext.field.Text textfield} it gains all of the functionality that text fields
107672 * provide, including getting and setting the value at runtime, validations and various events that are fired as the
107673 * user interacts with the component. Check out the {@link Ext.field.Text} docs to see the additional functionality
107674 * available.
107675 *
107676 * For more information regarding forms and fields, please review [Using Forms in Sencha Touch Guide](../../../components/forms.html)
107677 */
107678 Ext.define('Ext.field.Url', {
107679 extend: Ext.field.Text ,
107680 xtype: 'urlfield',
107681 alternateClassName: 'Ext.form.Url',
107682
107683 config: {
107684 /**
107685 * @cfg
107686 * @inheritdoc
107687 */
107688 autoCapitalize: false,
107689
107690 /**
107691 * @cfg
107692 * @inheritdoc
107693 */
107694 component: {
107695 type: 'url'
107696 }
107697 }
107698 });
107699
107700 /**
107701 * A FieldSet is a great way to visually separate elements of a form. It's normally used when you have a form with
107702 * fields that can be divided into groups - for example a customer's billing details in one fieldset and their shipping
107703 * address in another. A fieldset can be used inside a form or on its own elsewhere in your app. Fieldsets can
107704 * optionally have a title at the top and instructions at the bottom. Here's how we might create a FieldSet inside a
107705 * form:
107706 *
107707 * @example
107708 * Ext.create('Ext.form.Panel', {
107709 * fullscreen: true,
107710 * items: [
107711 * {
107712 * xtype: 'fieldset',
107713 * title: 'About You',
107714 * instructions: 'Tell us all about yourself',
107715 * items: [
107716 * {
107717 * xtype: 'textfield',
107718 * name : 'firstName',
107719 * label: 'First Name'
107720 * },
107721 * {
107722 * xtype: 'textfield',
107723 * name : 'lastName',
107724 * label: 'Last Name'
107725 * }
107726 * ]
107727 * }
107728 * ]
107729 * });
107730 *
107731 * Above we created a {@link Ext.form.Panel form} with a fieldset that contains two text fields. In this case, all
107732 * of the form fields are in the same fieldset, but for longer forms we may choose to use multiple fieldsets. We also
107733 * configured a {@link #title} and {@link #instructions} to give the user more information on filling out the form if
107734 * required.
107735 *
107736 * For more information regarding forms and fields, please review [Using Forms in Sencha Touch Guide](../../../components/forms.html)
107737 */
107738 Ext.define('Ext.form.FieldSet', {
107739 extend : Ext.Container ,
107740 alias : 'widget.fieldset',
107741
107742
107743 config: {
107744 /**
107745 * @cfg
107746 * @inheritdoc
107747 */
107748 baseCls: Ext.baseCSSPrefix + 'form-fieldset',
107749
107750 /**
107751 * @cfg {String} title
107752 * Optional fieldset title, rendered just above the grouped fields.
107753 *
107754 * ## Example
107755 *
107756 * Ext.create('Ext.form.Fieldset', {
107757 * fullscreen: true,
107758 *
107759 * title: 'Login',
107760 *
107761 * items: [{
107762 * xtype: 'textfield',
107763 * label: 'Email'
107764 * }]
107765 * });
107766 *
107767 * @accessor
107768 */
107769 title: null,
107770
107771 /**
107772 * @cfg {String} instructions
107773 * Optional fieldset instructions, rendered just below the grouped fields.
107774 *
107775 * ## Example
107776 *
107777 * Ext.create('Ext.form.Fieldset', {
107778 * fullscreen: true,
107779 *
107780 * instructions: 'Please enter your email address.',
107781 *
107782 * items: [{
107783 * xtype: 'textfield',
107784 * label: 'Email'
107785 * }]
107786 * });
107787 *
107788 * @accessor
107789 */
107790 instructions: null
107791 },
107792
107793 // @private
107794 applyTitle: function(title) {
107795 if (typeof title == 'string') {
107796 title = {title: title};
107797 }
107798
107799 Ext.applyIf(title, {
107800 docked : 'top',
107801 baseCls: this.getBaseCls() + '-title'
107802 });
107803
107804 return Ext.factory(title, Ext.Title, this._title);
107805 },
107806
107807 // @private
107808 updateTitle: function(newTitle, oldTitle) {
107809 if (newTitle) {
107810 this.add(newTitle);
107811 }
107812 if (oldTitle) {
107813 this.remove(oldTitle);
107814 }
107815 },
107816
107817 // @private
107818 getTitle: function() {
107819 var title = this._title;
107820
107821 if (title && title instanceof Ext.Title) {
107822 return title.getTitle();
107823 }
107824
107825 return title;
107826 },
107827
107828 // @private
107829 applyInstructions: function(instructions) {
107830 if (typeof instructions == 'string') {
107831 instructions = {title: instructions};
107832 }
107833
107834 Ext.applyIf(instructions, {
107835 docked : 'bottom',
107836 baseCls: this.getBaseCls() + '-instructions'
107837 });
107838
107839 return Ext.factory(instructions, Ext.Title, this._instructions);
107840 },
107841
107842 // @private
107843 updateInstructions: function(newInstructions, oldInstructions) {
107844 if (newInstructions) {
107845 this.add(newInstructions);
107846 }
107847 if (oldInstructions) {
107848 this.remove(oldInstructions);
107849 }
107850 },
107851
107852 // @private
107853 getInstructions: function() {
107854 var instructions = this._instructions;
107855
107856 if (instructions && instructions instanceof Ext.Title) {
107857 return instructions.getTitle();
107858 }
107859
107860 return instructions;
107861 },
107862
107863 /**
107864 * A convenient method to disable all fields in this FieldSet
107865 * @return {Ext.form.FieldSet} This FieldSet
107866 */
107867
107868 doSetDisabled: function(newDisabled) {
107869 this.getFieldsAsArray().forEach(function(field) {
107870 field.setDisabled(newDisabled);
107871 });
107872
107873 return this;
107874 },
107875
107876 /**
107877 * @private
107878 */
107879 getFieldsAsArray: function() {
107880 var fields = [],
107881 getFieldsFrom = function(item) {
107882 if (item.isField) {
107883 fields.push(item);
107884 }
107885
107886 if (item.isContainer) {
107887 item.getItems().each(getFieldsFrom);
107888 }
107889 };
107890
107891 this.getItems().each(getFieldsFrom);
107892
107893 return fields;
107894 }
107895 });
107896
107897 /**
107898 * The Form panel presents a set of form fields and provides convenient ways to load and save data. Usually a form
107899 * panel just contains the set of fields you want to display, ordered inside the items configuration like this:
107900 *
107901 * @example
107902 * var form = Ext.create('Ext.form.Panel', {
107903 * fullscreen: true,
107904 * items: [
107905 * {
107906 * xtype: 'textfield',
107907 * name: 'name',
107908 * label: 'Name'
107909 * },
107910 * {
107911 * xtype: 'emailfield',
107912 * name: 'email',
107913 * label: 'Email'
107914 * },
107915 * {
107916 * xtype: 'passwordfield',
107917 * name: 'password',
107918 * label: 'Password'
107919 * }
107920 * ]
107921 * });
107922 *
107923 * Here we just created a simple form panel which could be used as a registration form to sign up to your service. We
107924 * added a plain {@link Ext.field.Text text field} for the user's Name, an {@link Ext.field.Email email field} and
107925 * finally a {@link Ext.field.Password password field}. In each case we provided a {@link Ext.field.Field#name name}
107926 * config on the field so that we can identify it later on when we load and save data on the form.
107927 *
107928 * ##Loading data
107929 *
107930 * Using the form we created above, we can load data into it in a few different ways, the easiest is to use
107931 * {@link #setValues}:
107932 *
107933 * form.setValues({
107934 * name: 'Ed',
107935 * email: 'ed@sencha.com',
107936 * password: 'secret'
107937 * });
107938 *
107939 * It's also easy to load {@link Ext.data.Model Model} instances into a form - let's say we have a User model and want
107940 * to load a particular instance into our form:
107941 *
107942 * Ext.define('MyApp.model.User', {
107943 * extend: 'Ext.data.Model',
107944 * config: {
107945 * fields: ['name', 'email', 'password']
107946 * }
107947 * });
107948 *
107949 * var ed = Ext.create('MyApp.model.User', {
107950 * name: 'Ed',
107951 * email: 'ed@sencha.com',
107952 * password: 'secret'
107953 * });
107954 *
107955 * form.setRecord(ed);
107956 *
107957 * ##Retrieving form data
107958 *
107959 * Getting data out of the form panel is simple and is usually achieve via the {@link #getValues} method:
107960 *
107961 * var values = form.getValues();
107962 *
107963 * //values now looks like this:
107964 * {
107965 * name: 'Ed',
107966 * email: 'ed@sencha.com',
107967 * password: 'secret'
107968 * }
107969 *
107970 * It's also possible to listen to the change events on individual fields to get more timely notification of changes
107971 * that the user is making. Here we expand on the example above with the User model, updating the model as soon as
107972 * any of the fields are changed:
107973 *
107974 * var form = Ext.create('Ext.form.Panel', {
107975 * listeners: {
107976 * '> field': {
107977 * change: function(field, newValue, oldValue) {
107978 * ed.set(field.getName(), newValue);
107979 * }
107980 * }
107981 * },
107982 * items: [
107983 * {
107984 * xtype: 'textfield',
107985 * name: 'name',
107986 * label: 'Name'
107987 * },
107988 * {
107989 * xtype: 'emailfield',
107990 * name: 'email',
107991 * label: 'Email'
107992 * },
107993 * {
107994 * xtype: 'passwordfield',
107995 * name: 'password',
107996 * label: 'Password'
107997 * }
107998 * ]
107999 * });
108000 *
108001 * The above used a new capability of Sencha Touch 2.0, which enables you to specify listeners on child components of any
108002 * container. In this case, we attached a listener to the {@link Ext.field.Text#change change} event of each form
108003 * field that is a direct child of the form panel. Our listener gets the name of the field that fired the change event,
108004 * and updates our {@link Ext.data.Model Model} instance with the new value. For example, changing the email field
108005 * in the form will update the Model's email field.
108006 *
108007 * ##Submitting forms
108008 *
108009 * There are a few ways to submit form data. In our example above we have a Model instance that we have updated, giving
108010 * us the option to use the Model's {@link Ext.data.Model#save save} method to persist the changes back to our server,
108011 * without using a traditional form submission. Alternatively, we can send a normal browser form submit using the
108012 * {@link #method} method:
108013 *
108014 * form.submit({
108015 * url: 'url/to/submit/to',
108016 * method: 'POST',
108017 * success: function() {
108018 * alert('form submitted successfully!');
108019 * }
108020 * });
108021 *
108022 * In this case we provided the `url` to submit the form to inside the submit call - alternatively you can just set the
108023 * {@link #url} configuration when you create the form. We can specify other parameters (see {@link #method} for a
108024 * full list), including callback functions for success and failure, which are called depending on whether or not the
108025 * form submission was successful. These functions are usually used to take some action in your app after your data
108026 * has been saved to the server side.
108027 *
108028 * For more information regarding forms and fields, please review [Using Forms in Sencha Touch Guide](../../../components/forms.html)
108029 */
108030 Ext.define('Ext.form.Panel', {
108031 alternateClassName: 'Ext.form.FormPanel',
108032 extend : Ext.Panel ,
108033 xtype : 'formpanel',
108034
108035
108036 /**
108037 * @event submit
108038 * @preventable doSubmit
108039 * Fires upon successful (Ajax-based) form submission.
108040 * @param {Ext.form.Panel} this This FormPanel.
108041 * @param {Object} result The result object as returned by the server.
108042 * @param {Ext.EventObject} e The event object.
108043 */
108044
108045 /**
108046 * @event beforesubmit
108047 * @preventable doBeforeSubmit
108048 * Fires immediately preceding any Form submit action.
108049 * Implementations may adjust submitted form values or options prior to execution.
108050 * A return value of `false` from this listener will abort the submission
108051 * attempt (regardless of `standardSubmit` configuration).
108052 * @param {Ext.form.Panel} this This FormPanel.
108053 * @param {Object} values A hash collection of the qualified form values about to be submitted.
108054 * @param {Object} options Submission options hash (only available when `standardSubmit` is `false`).
108055 * @param {Ext.EventObject} e The event object if the form was submitted via a HTML5 form submit event.
108056 */
108057
108058 /**
108059 * @event exception
108060 * Fires when either the Ajax HTTP request reports a failure OR the server returns a `success:false`
108061 * response in the result payload.
108062 * @param {Ext.form.Panel} this This FormPanel.
108063 * @param {Object} result Either a failed Ext.data.Connection request object or a failed (logical) server.
108064 * response payload.
108065 */
108066
108067 config: {
108068 /**
108069 * @cfg {String} baseCls
108070 * @inheritdoc
108071 */
108072 baseCls: Ext.baseCSSPrefix + 'form',
108073
108074 /**
108075 * @cfg {Boolean} standardSubmit
108076 * Whether or not we want to perform a standard form submit.
108077 * @accessor
108078 */
108079 standardSubmit: false,
108080
108081 /**
108082 * @cfg {String} url
108083 * The default url for submit actions.
108084 * @accessor
108085 */
108086 url: null,
108087
108088 /**
108089 * @cfg (String} enctype
108090 * The enctype attribute for the form, specifies how the form should be encoded when submitting
108091 */
108092 enctype: null,
108093
108094 /**
108095 * @cfg {Object} baseParams
108096 * Optional hash of params to be sent (when `standardSubmit` configuration is `false`) on every submit.
108097 * @accessor
108098 */
108099 baseParams: null,
108100
108101 /**
108102 * @cfg {Object} submitOnAction
108103 * When this is set to `true`, the form will automatically submit itself whenever the `action`
108104 * event fires on a field in this form. The action event usually fires whenever you press
108105 * go or enter inside a textfield.
108106 * @accessor
108107 */
108108 submitOnAction: false,
108109
108110 /**
108111 * @cfg {Ext.data.Model} record The model instance of this form. Can by dynamically set at any time.
108112 * @accessor
108113 */
108114 record: null,
108115
108116 /**
108117 * @cfg {String} method
108118 * The method which this form will be submitted. `post` or `get`.
108119 */
108120 method: 'post',
108121
108122 /**
108123 * @cfg {Object} scrollable
108124 * Possible values are true, false, and null. The true value indicates that
108125 * users can scroll the panel. The false value disables scrolling, but developers
108126 * can enable it in the app. The null value indicates that the object cannot be
108127 * scrolled and that scrolling cannot be enabled for this object.
108128 *
108129 * Example:
108130 * title: 'Sliders',
108131 * xtype: 'formpanel',
108132 * iconCls: Ext.filterPlatform('blackberry') ? 'list' : null,
108133 * scrollable: true,
108134 * items: [ ...
108135 * @inheritdoc
108136 */
108137 scrollable: {
108138 translatable: {
108139 translationMethod: 'scrollposition'
108140 }
108141 },
108142
108143 /**
108144 * @cfg {Boolean} trackResetOnLoad
108145 * If set to true, {@link #reset}() resets to the last loaded or {@link #setValues}() data instead of
108146 * when the form was first created.
108147 */
108148 trackResetOnLoad:false,
108149
108150 /**
108151 * @cfg {Object} api
108152 * If specified, load and submit actions will be loaded and submitted via Ext.Direct. Methods which have been imported by
108153 * {@link Ext.direct.Manager} can be specified here to load and submit forms. API methods may also be
108154 * specified as strings and will be parsed into the actual functions when the first submit or load has occurred. Such as the following:
108155 *
108156 * api: {
108157 * load: App.ss.MyProfile.load,
108158 * submit: App.ss.MyProfile.submit
108159 * }
108160 *
108161 * api: {
108162 * load: 'App.ss.MyProfile.load',
108163 * submit: 'App.ss.MyProfile.submit'
108164 * }
108165 *
108166 * Load actions can use {@link #paramOrder} or {@link #paramsAsHash} to customize how the load method
108167 * is invoked. Submit actions will always use a standard form submit. The `formHandler` configuration
108168 * (see Ext.direct.RemotingProvider#action) must be set on the associated server-side method which has
108169 * been imported by {@link Ext.direct.Manager}.
108170 */
108171 api: null,
108172
108173 /**
108174 * @cfg {String/String[]} paramOrder
108175 * A list of params to be executed server side. Only used for the {@link #api} `load`
108176 * configuration.
108177 *
108178 * Specify the params in the order in which they must be executed on the
108179 * server-side as either (1) an Array of String values, or (2) a String of params
108180 * delimited by either whitespace, comma, or pipe. For example,
108181 * any of the following would be acceptable:
108182 *
108183 * paramOrder: ['param1','param2','param3']
108184 * paramOrder: 'param1 param2 param3'
108185 * paramOrder: 'param1,param2,param3'
108186 * paramOrder: 'param1|param2|param'
108187 */
108188 paramOrder: null,
108189
108190 /**
108191 * @cfg {Boolean} paramsAsHash
108192 * Only used for the {@link #api} `load` configuration. If true, parameters will be sent as a
108193 * single hash collection of named arguments. Providing a {@link #paramOrder} nullifies this
108194 * configuration.
108195 */
108196 paramsAsHash: null,
108197
108198 /**
108199 * @cfg {Number} timeout
108200 * Timeout for form actions in seconds.
108201 */
108202 timeout: 30,
108203
108204 /**
108205 * @cfg {Boolean} multipartDetection
108206 * If this is enabled the form will automatically detect the need to use 'multipart/form-data' during submission.
108207 */
108208 multipartDetection: true,
108209
108210 /**
108211 * @cfg {Boolean} enableSubmissionForm
108212 * The submission form is generated but never added to the dom. It is a submittable version of your form panel, allowing for fields
108213 * that are not simple textfields to be properly submitted to servers. It will also send values that are easier to parse
108214 * with server side code.
108215 *
108216 * If this is false we will attempt to subject the raw form inside the form panel.
108217 */
108218 enableSubmissionForm: true
108219 },
108220
108221 getElementConfig: function() {
108222 var config = this.callParent();
108223 config.tag = "form";
108224 // Added a submit input for standard form submission. This cannot have "display: none;" or it will not work
108225 config.children.push({
108226 tag: 'input',
108227 type: 'submit',
108228 style: 'visibility: hidden; width: 0; height: 0; position: absolute; right: 0; bottom: 0;'
108229 });
108230
108231 return config;
108232 },
108233
108234 // @private
108235 initialize: function() {
108236 var me = this;
108237 me.callParent();
108238
108239 me.element.on({
108240 submit: 'onSubmit',
108241 scope : me
108242 });
108243 },
108244
108245 applyEnctype: function(newValue) {
108246 var form = this.element.dom || null;
108247 if(form) {
108248 if (newValue) {
108249 form.setAttribute("enctype", newValue);
108250 } else {
108251 form.setAttribute("enctype");
108252 }
108253 }
108254 },
108255
108256 updateRecord: function(newRecord) {
108257 var fields, values, name;
108258
108259 if (newRecord && (fields = newRecord.fields)) {
108260 values = this.getValues();
108261 for (name in values) {
108262 if (values.hasOwnProperty(name) && fields.containsKey(name)) {
108263 newRecord.set(name, values[name]);
108264 }
108265 }
108266 }
108267 return this;
108268 },
108269
108270 /**
108271 * Loads matching fields from a model instance into this form.
108272 * @param {Ext.data.Model} record The model instance.
108273 * @return {Ext.form.Panel} This form.
108274 */
108275 setRecord: function(record) {
108276 var me = this;
108277
108278 if (record && record.data) {
108279 me.setValues(record.data);
108280 }
108281
108282 me._record = record;
108283
108284 return this;
108285 },
108286
108287 // @private
108288 onSubmit: function(e) {
108289 var me = this;
108290 if (e && !me.getStandardSubmit()) {
108291 e.stopEvent();
108292 } else {
108293 // Stop the submit event on the original for if we are swapping a form in
108294 if (me.getEnableSubmissionForm()) {
108295 e.stopEvent();
108296 }
108297 this.submit(null, e);
108298 }
108299 },
108300
108301 updateSubmitOnAction: function(newSubmitOnAction) {
108302 if (newSubmitOnAction) {
108303 this.on({
108304 action: 'onFieldAction',
108305 scope: this
108306 });
108307 } else {
108308 this.un({
108309 action: 'onFieldAction',
108310 scope: this
108311 });
108312 }
108313 },
108314
108315 // @private
108316 onFieldAction: function(field) {
108317 if (this.getSubmitOnAction()) {
108318 field.blur();
108319 this.submit();
108320 }
108321 },
108322
108323 /**
108324 * Performs a Ajax-based submission of form values (if {@link #standardSubmit} is false) or otherwise
108325 * executes a standard HTML Form submit action.
108326 *
108327 * **Notes**
108328 *
108329 * 1. Only the first parameter is implemented. Put all other parameters inside the first
108330 * parameter:
108331 *
108332 * submit({params: "" ,headers: "" etc.})
108333 *
108334 * 2. Submit example:
108335 *
108336 * myForm.submit({
108337 * url: 'PostMyData/To',
108338 * method: 'Post',
108339 * success: function() { Ext.Msg.alert("success"); },
108340 * failure: function() { Ext.Msg.alert("error"); }
108341 * });
108342 *
108343 * 3. Parameters and values only submit for a POST and not for a GET.
108344 *
108345 * @param {Object} options
108346 * The configuration when submitting this form.
108347 *
108348 * The following are the configurations when submitting via Ajax only:
108349 *
108350 * @param {String} options.url
108351 * The url for the action (defaults to the form's {@link #url}).
108352 *
108353 * @param {String} options.method
108354 * The form method to use (defaults to the form's {@link #method}, or POST if not defined).
108355 *
108356 * @param {Object} options.headers
108357 * Request headers to set for the action.
108358 *
108359 * @param {Boolean} [options.autoAbort=false]
108360 * `true` to abort any pending Ajax request prior to submission.
108361 * __Note:__ Has no effect when `{@link #standardSubmit}` is enabled.
108362 *
108363 * @param {Number} options.timeout
108364 * The number is seconds the loading will timeout in.
108365 *
108366 * The following are the configurations when loading via Ajax or Direct:
108367 *
108368 * @param {String/Object} options.params
108369 * The params to pass when submitting this form (defaults to this forms {@link #baseParams}).
108370 * Parameters are encoded as standard HTTP parameters using {@link Ext#urlEncode}.
108371 *
108372 * @param {Boolean} [options.submitDisabled=false]
108373 * `true` to submit all fields regardless of disabled state.
108374 * __Note:__ Has no effect when `{@link #standardSubmit}` is enabled.
108375 *
108376 * @param {String/Object} [options.waitMsg]
108377 * If specified, the value which is passed to the loading {@link #masked mask}. See {@link #masked} for
108378 * more information.
108379 *
108380 * @param {Function} options.success
108381 * The callback that will be invoked after a successful response. A response is successful if
108382 * a response is received from the server and is a JSON object where the `success` property is set
108383 * to `true`, `{"success": true}`.
108384 *
108385 * The function is passed the following parameters and can be used for submitting via Ajax or Direct:
108386 *
108387 * @param {Ext.form.Panel} options.success.form
108388 * The {@link Ext.form.Panel} that requested the action.
108389 *
108390 * @param {Object/Ext.direct.Event} options.success.result
108391 * The result object returned by the server as a result of the submit request. If the submit is sent using Ext.Direct,
108392 * this will return the {@link Ext.direct.Event} instance, otherwise will return an Object.
108393 *
108394 * @param {Object} options.success.data
108395 * The parsed data returned by the server.
108396 *
108397 * @param {Function} options.failure
108398 * The callback that will be invoked after a failed transaction attempt.
108399 *
108400 * The function is passed the following parameters and can be used for submitting via Ajax or Direct:
108401 *
108402 * @param {Ext.form.Panel} options.failure.form
108403 * The {@link Ext.form.Panel} that requested the submit.
108404 *
108405 * @param {Ext.form.Panel} options.failure.result
108406 * The failed response or result object returned by the server which performed the operation.
108407 *
108408 * @param {Object} options.success.data
108409 * The parsed data returned by the server.
108410 *
108411 * @param {Object} options.scope
108412 * The scope in which to call the callback functions (The `this` reference for the callback functions).
108413 *
108414 * @return {Ext.data.Connection} The request object if the {@link #standardSubmit} config is false.
108415 * If the standardSubmit config is true, then the return value is undefined.
108416 */
108417 submit: function(options, e) {
108418 options = options || {};
108419
108420 var me = this,
108421 formValues = me.getValues(me.getStandardSubmit() || !options.submitDisabled),
108422 form = me.element.dom || {};
108423
108424 if(this.getEnableSubmissionForm()) {
108425 form = this.createSubmissionForm(form, formValues);
108426 }
108427
108428 options = Ext.apply({
108429 url : me.getUrl() || form.action,
108430 submit: false,
108431 form: form,
108432 method : me.getMethod() || form.method || 'post',
108433 autoAbort : false,
108434 params : null,
108435 waitMsg : null,
108436 headers : null,
108437 success : null,
108438 failure : null
108439 }, options || {});
108440
108441 return me.fireAction('beforesubmit', [me, formValues, options, e], 'doBeforeSubmit');
108442 },
108443
108444 createSubmissionForm: function(form, values) {
108445 var fields = this.getFields(),
108446 name, input, field, fileinputElement, inputComponent;
108447
108448 if(form.nodeType === 1) {
108449 form = form.cloneNode(false);
108450
108451 for (name in values) {
108452 input = document.createElement("input");
108453 input.setAttribute("type", "text");
108454 input.setAttribute("name", name);
108455 input.setAttribute("value", values[name]);
108456 form.appendChild(input);
108457 }
108458 }
108459
108460 for (name in fields) {
108461 if (fields.hasOwnProperty(name)) {
108462 field = fields[name];
108463 if(field.isFile) {
108464 if(!form.$fileswap) form.$fileswap = [];
108465
108466 inputComponent = field.getComponent().input;
108467 fileinputElement = inputComponent.dom;
108468 input = fileinputElement.cloneNode(true);
108469 fileinputElement.parentNode.insertBefore(input, fileinputElement.nextSibling);
108470 form.appendChild(fileinputElement);
108471 form.$fileswap.push({original: fileinputElement, placeholder: input});
108472 } else if(field.isPassword) {
108473 if(field.getComponent().getType !== "password") {
108474 field.setRevealed(false);
108475 }
108476 }
108477 }
108478 }
108479
108480 return form;
108481 },
108482
108483 doBeforeSubmit: function(me, formValues, options) {
108484 var form = options.form || {},
108485 multipartDetected = false;
108486
108487 if(this.getMultipartDetection() === true) {
108488 this.getFieldsAsArray().forEach(function(field) {
108489 if(field.isFile === true) {
108490 multipartDetected = true;
108491 return false;
108492 }
108493 });
108494
108495 if(multipartDetected) {
108496 form.setAttribute("enctype", "multipart/form-data");
108497 }
108498 }
108499
108500 if(options.enctype) {
108501 form.setAttribute("enctype", options.enctype);
108502 }
108503
108504 if (me.getStandardSubmit()) {
108505 if (options.url && Ext.isEmpty(form.action)) {
108506 form.action = options.url;
108507 }
108508
108509 // Spinner fields must have their components enabled *before* submitting or else the value
108510 // will not be posted.
108511 var fields = this.query('spinnerfield'),
108512 ln = fields.length,
108513 i, field;
108514
108515 for (i = 0; i < ln; i++) {
108516 field = fields[i];
108517 if (!field.getDisabled()) {
108518 field.getComponent().setDisabled(false);
108519 }
108520 }
108521
108522 form.method = (options.method || form.method).toLowerCase();
108523 form.submit();
108524 } else {
108525 var api = me.getApi(),
108526 url = options.url || me.getUrl(),
108527 scope = options.scope || me,
108528 waitMsg = options.waitMsg,
108529 failureFn = function(response, responseText) {
108530 if (Ext.isFunction(options.failure)) {
108531 options.failure.call(scope, me, response, responseText);
108532 }
108533
108534 me.fireEvent('exception', me, response);
108535 },
108536 successFn = function(response, responseText) {
108537 if (Ext.isFunction(options.success)) {
108538 options.success.call(options.scope || me, me, response, responseText);
108539 }
108540
108541 me.fireEvent('submit', me, response);
108542 },
108543 submit;
108544
108545 if (options.waitMsg) {
108546 if (typeof waitMsg === 'string') {
108547 waitMsg = {
108548 xtype : 'loadmask',
108549 message : waitMsg
108550 };
108551 }
108552
108553 me.setMasked(waitMsg);
108554 }
108555
108556 if (api) {
108557 submit = api.submit;
108558
108559 if (typeof submit === 'string') {
108560 submit = Ext.direct.Manager.parseMethod(submit);
108561
108562 if (submit) {
108563 api.submit = submit;
108564 }
108565 }
108566
108567 if (submit) {
108568 return submit(this.element, function(data, response, success) {
108569 me.setMasked(false);
108570
108571 if (success) {
108572 if (data.success) {
108573 successFn(response, data);
108574 } else {
108575 failureFn(response, data);
108576 }
108577 } else {
108578 failureFn(response, data);
108579 }
108580 }, this);
108581 }
108582 } else {
108583 var request = Ext.merge({},
108584 {
108585 url: url,
108586 timeout: this.getTimeout() * 1000,
108587 form: form,
108588 scope: me
108589 },
108590 options
108591 );
108592 delete request.success;
108593 delete request.failure;
108594
108595 request.params = Ext.merge(me.getBaseParams() || {}, options.params);
108596 request.header = Ext.apply(
108597 {
108598 'Content-Type' : 'application/x-www-form-urlencoded; charset=UTF-8'
108599 },
108600 options.headers || {}
108601 );
108602 request.callback = function(callbackOptions, success, response) {
108603 var responseText = response.responseText,
108604 responseXML = response.responseXML,
108605 statusResult = Ext.Ajax.parseStatus(response.status, response);
108606
108607 if(form.$fileswap) {
108608 var original, placeholder;
108609 Ext.each(form.$fileswap, function(item) {
108610 original = item.original;
108611 placeholder = item.placeholder;
108612
108613 placeholder.parentNode.insertBefore(original, placeholder.nextSibling);
108614 placeholder.parentNode.removeChild(placeholder);
108615 });
108616 form.$fileswap = null;
108617 delete form.$fileswap;
108618 }
108619
108620 me.setMasked(false);
108621
108622 if(response.success === false) success = false;
108623 if (success) {
108624 if (statusResult && responseText && responseText.length == 0) {
108625 success = true;
108626 } else {
108627 if(!Ext.isEmpty(response.responseBytes)) {
108628 success = statusResult.success;
108629 }else {
108630 if(Ext.isString(responseText) && response.request.options.responseType === "text") {
108631 response.success = true;
108632 } else if(Ext.isString(responseText)) {
108633 try {
108634 response = Ext.decode(responseText);
108635 }catch (e){
108636 response.success = false;
108637 response.error = e;
108638 response.message = e.message;
108639 }
108640 } else if(Ext.isSimpleObject(responseText)) {
108641 response = responseText;
108642 Ext.applyIf(response, {success:true});
108643 }
108644
108645 if(!Ext.isEmpty(responseXML)){
108646 response.success = true;
108647 }
108648 success = !!response.success;
108649 }
108650 }
108651 if (success) {
108652 successFn(response, responseText);
108653 } else {
108654 failureFn(response, responseText);
108655 }
108656 }
108657 else {
108658 failureFn(response, responseText);
108659 }
108660 };
108661
108662 if(Ext.feature.has.XHR2 && request.xhr2) {
108663 delete request.form;
108664 var formData = new FormData(form);
108665 if (request.params) {
108666 Ext.iterate(request.params, function(name, value) {
108667 if (Ext.isArray(value)) {
108668 Ext.each(value, function(v) {
108669 formData.append(name, v);
108670 });
108671 } else {
108672 formData.append(name, value);
108673 }
108674 });
108675 delete request.params;
108676 }
108677 request.data = formData;
108678 }
108679
108680 return Ext.Ajax.request(request);
108681 }
108682 }
108683 },
108684
108685 /**
108686 * Performs an Ajax or Ext.Direct call to load values for this form.
108687 *
108688 * @param {Object} options
108689 * The configuration when loading this form.
108690 *
108691 * The following are the configurations when loading via Ajax only:
108692 *
108693 * @param {String} options.url
108694 * The url for the action (defaults to the form's {@link #url}).
108695 *
108696 * @param {String} options.method
108697 * The form method to use (defaults to the form's {@link #method}, or GET if not defined).
108698 *
108699 * @param {Object} options.headers
108700 * Request headers to set for the action.
108701 *
108702 * @param {Number} options.timeout
108703 * The number is seconds the loading will timeout in.
108704 *
108705 * The following are the configurations when loading via Ajax or Direct:
108706 *
108707 * @param {Boolean} [options.autoAbort=false]
108708 * `true` to abort any pending Ajax request prior to loading.
108709 *
108710 * @param {String/Object} options.params
108711 * The params to pass when submitting this form (defaults to this forms {@link #baseParams}).
108712 * Parameters are encoded as standard HTTP parameters using {@link Ext#urlEncode}.
108713 *
108714 * @param {String/Object} [options.waitMsg]
108715 * If specified, the value which is passed to the loading {@link #masked mask}. See {@link #masked} for
108716 * more information.
108717 *
108718 * @param {Function} options.success
108719 * The callback that will be invoked after a successful response. A response is successful if
108720 * a response is received from the server and is a JSON object where the `success` property is set
108721 * to `true`, `{"success": true}`.
108722 *
108723 * The function is passed the following parameters and can be used for loading via Ajax or Direct:
108724 *
108725 * @param {Ext.form.Panel} options.success.form
108726 * The {@link Ext.form.Panel} that requested the load.
108727 *
108728 * @param {Object/Ext.direct.Event} options.success.result
108729 * The result object returned by the server as a result of the load request. If the loading was done via Ext.Direct,
108730 * will return the {@link Ext.direct.Event} instance, otherwise will return an Object.
108731 *
108732 * @param {Object} options.success.data
108733 * The parsed data returned by the server.
108734 *
108735 * @param {Function} options.failure
108736 * The callback that will be invoked after a failed transaction attempt.
108737 *
108738 * The function is passed the following parameters and can be used for loading via Ajax or Direct:
108739 *
108740 * @param {Ext.form.Panel} options.failure.form
108741 * The {@link Ext.form.Panel} that requested the load.
108742 *
108743 * @param {Ext.form.Panel} options.failure.result
108744 * The failed response or result object returned by the server which performed the operation.
108745 *
108746 * @param {Object} options.success.data
108747 * The parsed data returned by the server.
108748 *
108749 * @param {Object} options.scope
108750 * The scope in which to call the callback functions (The `this` reference for the callback functions).
108751 *
108752 * @return {Ext.data.Connection} The request object.
108753 */
108754 load : function(options) {
108755 options = options || {};
108756
108757 var me = this,
108758 api = me.getApi(),
108759 url = me.getUrl() || options.url,
108760 waitMsg = options.waitMsg,
108761 successFn = function(response, data) {
108762 me.setValues(data.data);
108763
108764 if (Ext.isFunction(options.success)) {
108765 options.success.call(options.scope || me, me, response, data);
108766 }
108767
108768 me.fireEvent('load', me, response);
108769 },
108770 failureFn = function(response, data) {
108771 if (Ext.isFunction(options.failure)) {
108772 options.failure.call(scope, me, response, data);
108773 }
108774
108775 me.fireEvent('exception', me, response);
108776 },
108777 load, method, args;
108778
108779 if (options.waitMsg) {
108780 if (typeof waitMsg === 'string') {
108781 waitMsg = {
108782 xtype : 'loadmask',
108783 message : waitMsg
108784 };
108785 }
108786
108787 me.setMasked(waitMsg);
108788 }
108789
108790 if (api) {
108791 load = api.load;
108792
108793 if (typeof load === 'string') {
108794 load = Ext.direct.Manager.parseMethod(load);
108795
108796 if (load) {
108797 api.load = load;
108798 }
108799 }
108800
108801 if (load) {
108802 method = load.directCfg.method;
108803 args = method.getArgs(me.getParams(options.params), me.getParamOrder(), me.getParamsAsHash());
108804
108805 args.push(function(data, response, success) {
108806 me.setMasked(false);
108807
108808 if (success) {
108809 successFn(response, data);
108810 } else {
108811 failureFn(response, data);
108812 }
108813 }, me);
108814
108815 return load.apply(window, args);
108816 }
108817 } else if (url) {
108818 return Ext.Ajax.request({
108819 url: url,
108820 timeout: (options.timeout || this.getTimeout()) * 1000,
108821 method: options.method || 'GET',
108822 autoAbort: options.autoAbort,
108823 headers: Ext.apply(
108824 {
108825 'Content-Type' : 'application/x-www-form-urlencoded; charset=UTF-8'
108826 },
108827 options.headers || {}
108828 ),
108829 callback: function(callbackOptions, success, response) {
108830 var responseText = response.responseText,
108831 statusResult = Ext.Ajax.parseStatus(response.status, response);
108832
108833 me.setMasked(false);
108834
108835 if (success) {
108836 if (statusResult && responseText.length == 0) {
108837 success = true;
108838 } else {
108839 response = Ext.decode(responseText);
108840 success = !!response.success;
108841 }
108842 if (success) {
108843 successFn(response, responseText);
108844 } else {
108845 failureFn(response, responseText);
108846 }
108847 }
108848 else {
108849 failureFn(response, responseText);
108850 }
108851 }
108852 });
108853 }
108854 },
108855
108856 //@private
108857 getParams : function(params) {
108858 return Ext.apply({}, params, this.getBaseParams());
108859 },
108860
108861 /**
108862 * Sets the values of form fields in bulk. Example usage:
108863 *
108864 * myForm.setValues({
108865 * name: 'Ed',
108866 * crazy: true,
108867 * username: 'edspencer'
108868 * });
108869 *
108870 * If there groups of checkbox fields with the same name, pass their values in an array. For example:
108871 *
108872 * myForm.setValues({
108873 * name: 'Jacky',
108874 * crazy: false,
108875 * hobbies: [
108876 * 'reading',
108877 * 'cooking',
108878 * 'gaming'
108879 * ]
108880 * });
108881 *
108882 * @param {Object} values field name => value mapping object.
108883 * @return {Ext.form.Panel} This form.
108884 */
108885 setValues: function(values) {
108886 var fields = this.getFields(),
108887 me = this,
108888 name, field, value, ln, i, f;
108889
108890 values = values || {};
108891
108892 for (name in values) {
108893 if (values.hasOwnProperty(name)) {
108894 field = fields[name];
108895 value = values[name];
108896
108897 if (field) {
108898 // If there are multiple fields with the same name. Checkboxes, radio fields and maybe event just normal fields..
108899 if (Ext.isArray(field)) {
108900 ln = field.length;
108901
108902 // Loop through each of the fields
108903 for (i = 0; i < ln; i++) {
108904 f = field[i];
108905
108906 if (f.isRadio) {
108907 // If it is a radio field just use setGroupValue which will handle all of the radio fields
108908 f.setGroupValue(value);
108909 break;
108910 } else if (f.isCheckbox) {
108911 if (Ext.isArray(value)) {
108912 f.setChecked((value.indexOf(f._value) != -1));
108913 } else {
108914 f.setChecked((value == f._value));
108915 }
108916 } else {
108917 // If it is a bunch of fields with the same name, check if the value is also an array, so we can map it
108918 // to each field
108919 if (Ext.isArray(value)) {
108920 f.setValue(value[i]);
108921 }
108922 }
108923 }
108924 } else {
108925 if (field.isRadio || field.isCheckbox) {
108926 // If the field is a radio or a checkbox
108927 field.setChecked(value);
108928 } else {
108929 // If just a normal field
108930 field.setValue(value);
108931 }
108932 }
108933
108934 if (me.getTrackResetOnLoad()) {
108935 field.resetOriginalValue();
108936 }
108937 }
108938 }
108939 }
108940
108941 return this;
108942 },
108943
108944 /**
108945 * Returns an object containing the value of each field in the form, keyed to the field's name.
108946 * For groups of checkbox fields with the same name, it will be arrays of values. For example:
108947 *
108948 * {
108949 * name: "Jacky Nguyen", // From a TextField
108950 * favorites: [
108951 * 'pizza',
108952 * 'noodle',
108953 * 'cake'
108954 * ]
108955 * }
108956 *
108957 * @param {Boolean} [enabled] `true` to return only enabled fields.
108958 * @param {Boolean} [all] `true` to return all fields even if they don't have a
108959 * {@link Ext.field.Field#name name} configured.
108960 * @return {Object} Object mapping field name to its value.
108961 */
108962 getValues: function(enabled, all) {
108963 var fields = this.getFields(),
108964 values = {},
108965 isArray = Ext.isArray,
108966 field, value, addValue, bucket, name, ln, i;
108967
108968 // Function which you give a field and a name, and it will add it into the values
108969 // object accordingly
108970 addValue = function(field, name) {
108971 if (!all && (!name || name === 'null') || field.isFile) {
108972 return;
108973 }
108974
108975 if (field.isCheckbox) {
108976 value = field.getSubmitValue();
108977 } else {
108978 value = field.getValue();
108979 }
108980
108981
108982 if (!(enabled && field.getDisabled())) {
108983 // RadioField is a special case where the value returned is the fields valUE
108984 // ONLY if it is checked
108985 if (field.isRadio) {
108986 if (field.isChecked()) {
108987 values[name] = value;
108988 }
108989 } else {
108990 // Check if the value already exists
108991 bucket = values[name];
108992 if (!Ext.isEmpty(bucket)) {
108993 // if it does and it isn't an array, we need to make it into an array
108994 // so we can push more
108995 if (!isArray(bucket)) {
108996 bucket = values[name] = [bucket];
108997 }
108998
108999 // Check if it is an array
109000 if (isArray(value)) {
109001 // Concat it into the other values
109002 bucket = values[name] = bucket.concat(value);
109003 } else {
109004 // If it isn't an array, just pushed more values
109005 bucket.push(value);
109006 }
109007 } else {
109008 values[name] = value;
109009 }
109010 }
109011 }
109012 };
109013
109014 // Loop through each of the fields, and add the values for those fields.
109015 for (name in fields) {
109016 if (fields.hasOwnProperty(name)) {
109017 field = fields[name];
109018
109019 if (isArray(field)) {
109020 ln = field.length;
109021 for (i = 0; i < ln; i++) {
109022 addValue(field[i], name);
109023 }
109024 } else {
109025 addValue(field, name);
109026 }
109027 }
109028 }
109029
109030 return values;
109031 },
109032
109033 /**
109034 * Resets all fields in the form back to their original values.
109035 * @return {Ext.form.Panel} This form.
109036 */
109037 reset: function() {
109038 this.getFieldsAsArray().forEach(function(field) {
109039 field.reset();
109040 });
109041
109042 return this;
109043 },
109044
109045 /**
109046 * A convenient method to disable all fields in this form.
109047 * @return {Ext.form.Panel} This form.
109048 */
109049 doSetDisabled: function(newDisabled) {
109050 this.getFieldsAsArray().forEach(function(field) {
109051 field.setDisabled(newDisabled);
109052 });
109053
109054 return this;
109055 },
109056
109057 /**
109058 * @private
109059 */
109060 getFieldsAsArray: function() {
109061 var fields = [],
109062 getFieldsFrom = function(item) {
109063 if (item.isField) {
109064 fields.push(item);
109065 }
109066
109067 if (item.isContainer) {
109068 item.getItems().each(getFieldsFrom);
109069 }
109070 };
109071
109072 this.getItems().each(getFieldsFrom);
109073
109074 return fields;
109075 },
109076
109077 /**
109078 * Returns all {@link Ext.field.Field field} instances inside this form.
109079 * @param {Boolean} byName return only fields that match the given name, otherwise return all fields.
109080 * @return {Object/Array} All field instances, mapped by field name; or an array if `byName` is passed.
109081 */
109082 getFields: function(byName) {
109083 var fields = {},
109084 itemName;
109085
109086 var getFieldsFrom = function(item) {
109087 if (item.isField) {
109088 itemName = item.getName();
109089
109090 if ((byName && itemName == byName) || typeof byName == 'undefined') {
109091 if (fields.hasOwnProperty(itemName)) {
109092 if (!Ext.isArray(fields[itemName])) {
109093 fields[itemName] = [fields[itemName]];
109094 }
109095
109096 fields[itemName].push(item);
109097 } else {
109098 fields[itemName] = item;
109099 }
109100 }
109101
109102 }
109103
109104 if (item.isContainer) {
109105 item.items.each(getFieldsFrom);
109106 }
109107 };
109108
109109 this.getItems().each(getFieldsFrom);
109110
109111 return (byName) ? (fields[byName] || []) : fields;
109112 },
109113
109114 /**
109115 * Returns an array of fields in this formpanel.
109116 * @return {Ext.field.Field[]} An array of fields in this form panel.
109117 * @private
109118 */
109119 getFieldsArray: function() {
109120 var fields = [];
109121
109122 var getFieldsFrom = function(item) {
109123 if (item.isField) {
109124 fields.push(item);
109125 }
109126
109127 if (item.isContainer) {
109128 item.items.each(getFieldsFrom);
109129 }
109130 };
109131
109132 this.items.each(getFieldsFrom);
109133
109134 return fields;
109135 },
109136
109137 getFieldsFromItem: Ext.emptyFn,
109138
109139 /**
109140 * Shows a generic/custom mask over a designated Element.
109141 * @param {String/Object} cfg Either a string message or a configuration object supporting
109142 * the following options:
109143 *
109144 * {
109145 * message : 'Please Wait',
109146 * cls : 'form-mask'
109147 * }
109148 *
109149 * @param {Object} target
109150 * @return {Ext.form.Panel} This form
109151 * @deprecated 2.0.0 Please use {@link #setMasked} instead.
109152 */
109153 showMask: function(cfg, target) {
109154 //<debug>
109155 Ext.Logger.warn('showMask is now deprecated. Please use Ext.form.Panel#setMasked instead');
109156 //</debug>
109157
109158 cfg = Ext.isObject(cfg) ? cfg.message : cfg;
109159
109160 if (cfg) {
109161 this.setMasked({
109162 xtype: 'loadmask',
109163 message: cfg
109164 });
109165 } else {
109166 this.setMasked(true);
109167 }
109168
109169 return this;
109170 },
109171
109172 /**
109173 * Hides a previously shown wait mask (See {@link #showMask}).
109174 * @return {Ext.form.Panel} this
109175 * @deprecated 2.0.0 Please use {@link #unmask} or {@link #setMasked} instead.
109176 */
109177 hideMask: function() {
109178 this.setMasked(false);
109179 return this;
109180 },
109181
109182 /**
109183 * Returns the currently focused field
109184 * @return {Ext.field.Field} The currently focused field, if one is focused or `null`.
109185 * @private
109186 */
109187 getFocusedField: function() {
109188 var fields = this.getFieldsArray(),
109189 ln = fields.length,
109190 field, i;
109191
109192 for (i = 0; i < ln; i++) {
109193 field = fields[i];
109194 if (field.isFocused) {
109195 return field;
109196 }
109197 }
109198
109199 return null;
109200 },
109201
109202 /**
109203 * @return {Boolean/Ext.field.Field} The next field if one exists, or `false`.
109204 * @private
109205 */
109206 getNextField: function() {
109207 var fields = this.getFieldsArray(),
109208 focusedField = this.getFocusedField(),
109209 index;
109210
109211 if (focusedField) {
109212 index = fields.indexOf(focusedField);
109213
109214 if (index !== fields.length - 1) {
109215 index++;
109216 return fields[index];
109217 }
109218 }
109219
109220 return false;
109221 },
109222
109223 /**
109224 * Tries to focus the next field in the form, if there is currently a focused field.
109225 * @return {Boolean/Ext.field.Field} The next field that was focused, or `false`.
109226 * @private
109227 */
109228 focusNextField: function() {
109229 var field = this.getNextField();
109230 if (field) {
109231 field.focus();
109232 return field;
109233 }
109234
109235 return false;
109236 },
109237
109238 /**
109239 * @private
109240 * @return {Boolean/Ext.field.Field} The next field if one exists, or `false`.
109241 */
109242 getPreviousField: function() {
109243 var fields = this.getFieldsArray(),
109244 focusedField = this.getFocusedField(),
109245 index;
109246
109247 if (focusedField) {
109248 index = fields.indexOf(focusedField);
109249
109250 if (index !== 0) {
109251 index--;
109252 return fields[index];
109253 }
109254 }
109255
109256 return false;
109257 },
109258
109259 /**
109260 * Tries to focus the previous field in the form, if there is currently a focused field.
109261 * @return {Boolean/Ext.field.Field} The previous field that was focused, or `false`.
109262 * @private
109263 */
109264 focusPreviousField: function() {
109265 var field = this.getPreviousField();
109266 if (field) {
109267 field.focus();
109268 return field;
109269 }
109270
109271 return false;
109272 }
109273 }, function() {
109274
109275 });
109276
109277 /**
109278 * @private
109279 */
109280 Ext.define('Ext.fx.Easing', {
109281
109282
109283 constructor: function(easing) {
109284 return Ext.factory(easing, Ext.fx.easing.Linear, null, 'easing');
109285 }
109286 });
109287
109288 /**
109289 * @private
109290 */
109291 Ext.define('Ext.fx.runner.Css', {
109292 extend: Ext.Evented ,
109293
109294
109295
109296
109297
109298 prefixedProperties: {
109299 'transform' : true,
109300 'transform-origin' : true,
109301 'perspective' : true,
109302 'transform-style' : true,
109303 'transition' : true,
109304 'transition-property' : true,
109305 'transition-duration' : true,
109306 'transition-timing-function': true,
109307 'transition-delay' : true,
109308 'animation' : true,
109309 'animation-name' : true,
109310 'animation-duration' : true,
109311 'animation-iteration-count' : true,
109312 'animation-direction' : true,
109313 'animation-timing-function' : true,
109314 'animation-delay' : true
109315 },
109316
109317 lengthProperties: {
109318 'top' : true,
109319 'right' : true,
109320 'bottom' : true,
109321 'left' : true,
109322 'width' : true,
109323 'height' : true,
109324 'max-height' : true,
109325 'max-width' : true,
109326 'min-height' : true,
109327 'min-width' : true,
109328 'margin-bottom' : true,
109329 'margin-left' : true,
109330 'margin-right' : true,
109331 'margin-top' : true,
109332 'padding-bottom' : true,
109333 'padding-left' : true,
109334 'padding-right' : true,
109335 'padding-top' : true,
109336 'border-bottom-width': true,
109337 'border-left-width' : true,
109338 'border-right-width' : true,
109339 'border-spacing' : true,
109340 'border-top-width' : true,
109341 'border-width' : true,
109342 'outline-width' : true,
109343 'letter-spacing' : true,
109344 'line-height' : true,
109345 'text-indent' : true,
109346 'word-spacing' : true,
109347 'font-size' : true,
109348 'translate' : true,
109349 'translateX' : true,
109350 'translateY' : true,
109351 'translateZ' : true,
109352 'translate3d' : true
109353 },
109354
109355 durationProperties: {
109356 'transition-duration' : true,
109357 'transition-delay' : true,
109358 'animation-duration' : true,
109359 'animation-delay' : true
109360 },
109361
109362 angleProperties: {
109363 rotate : true,
109364 rotateX : true,
109365 rotateY : true,
109366 rotateZ : true,
109367 skew : true,
109368 skewX : true,
109369 skewY : true
109370 },
109371
109372 lengthUnitRegex: /([a-z%]*)$/,
109373
109374 DEFAULT_UNIT_LENGTH: 'px',
109375
109376 DEFAULT_UNIT_ANGLE: 'deg',
109377
109378 DEFAULT_UNIT_DURATION: 'ms',
109379
109380 formattedNameCache: {},
109381
109382 constructor: function() {
109383 var supports3dTransform = Ext.feature.has.Css3dTransforms;
109384
109385 if (supports3dTransform) {
109386 this.transformMethods = ['translateX', 'translateY', 'translateZ', 'rotate', 'rotateX', 'rotateY', 'rotateZ', 'skewX', 'skewY', 'scaleX', 'scaleY', 'scaleZ'];
109387 }
109388 else {
109389 this.transformMethods = ['translateX', 'translateY', 'rotate', 'skewX', 'skewY', 'scaleX', 'scaleY'];
109390 }
109391
109392 this.vendorPrefix = Ext.browser.getStyleDashPrefix();
109393
109394 this.ruleStylesCache = {};
109395
109396 return this;
109397 },
109398
109399 getStyleSheet: function() {
109400 var styleSheet = this.styleSheet,
109401 styleElement, styleSheets;
109402
109403 if (!styleSheet) {
109404 styleElement = document.createElement('style');
109405 styleElement.type = 'text/css';
109406
109407 (document.head || document.getElementsByTagName('head')[0]).appendChild(styleElement);
109408
109409 styleSheets = document.styleSheets;
109410
109411 this.styleSheet = styleSheet = styleSheets[styleSheets.length - 1];
109412 }
109413
109414 return styleSheet;
109415 },
109416
109417 applyRules: function(selectors) {
109418 var styleSheet = this.getStyleSheet(),
109419 ruleStylesCache = this.ruleStylesCache,
109420 rules = styleSheet.cssRules,
109421 selector, properties, ruleStyle,
109422 ruleStyleCache, rulesLength, name, value;
109423
109424 for (selector in selectors) {
109425 properties = selectors[selector];
109426
109427 ruleStyle = ruleStylesCache[selector];
109428
109429 if (ruleStyle === undefined) {
109430 rulesLength = rules.length;
109431 styleSheet.insertRule(selector + '{}', rulesLength);
109432 ruleStyle = ruleStylesCache[selector] = rules.item(rulesLength).style;
109433 }
109434
109435 ruleStyleCache = ruleStyle.$cache;
109436
109437 if (!ruleStyleCache) {
109438 ruleStyleCache = ruleStyle.$cache = {};
109439 }
109440
109441 for (name in properties) {
109442 value = this.formatValue(properties[name], name);
109443 name = this.formatName(name);
109444
109445 if (ruleStyleCache[name] !== value) {
109446 ruleStyleCache[name] = value;
109447
109448 if (value === null) {
109449 ruleStyle.removeProperty(name);
109450 }
109451 else {
109452 ruleStyle.setProperty(name, value, 'important');
109453 }
109454 }
109455 }
109456 }
109457
109458 return this;
109459 },
109460
109461 applyStyles: function(styles) {
109462 var id, element, elementStyle, properties, name, value;
109463
109464 for (id in styles) {
109465 if (styles.hasOwnProperty(id)) {
109466 element = document.getElementById(id);
109467
109468 if (!element) {
109469 return this;
109470 }
109471
109472 elementStyle = element.style;
109473
109474 properties = styles[id];
109475 for (name in properties) {
109476 if (properties.hasOwnProperty(name)) {
109477 value = this.formatValue(properties[name], name);
109478 name = this.formatName(name);
109479
109480 if (value === null) {
109481 elementStyle.removeProperty(name);
109482 }
109483 else {
109484 elementStyle.setProperty(name, value, 'important');
109485 }
109486 }
109487 }
109488 }
109489 }
109490
109491 return this;
109492 },
109493
109494 formatName: function(name) {
109495 var cache = this.formattedNameCache,
109496 formattedName = cache[name];
109497
109498 if (!formattedName) {
109499 if ((Ext.os.is.Tizen || !Ext.feature.has.CssTransformNoPrefix) && this.prefixedProperties[name]) {
109500 formattedName = this.vendorPrefix + name;
109501 }
109502 else {
109503 formattedName = name;
109504 }
109505
109506 cache[name] = formattedName;
109507 }
109508
109509 return formattedName;
109510 },
109511
109512 formatValue: function(value, name) {
109513 var type = typeof value,
109514 lengthUnit = this.DEFAULT_UNIT_LENGTH,
109515 transformMethods,
109516 method, i, ln,
109517 transformValues, values, unit;
109518
109519 if (value === null) {
109520 return '';
109521 }
109522
109523 if (type == 'string') {
109524 if (this.lengthProperties[name]) {
109525 unit = value.match(this.lengthUnitRegex)[1];
109526
109527 if (unit.length > 0) {
109528 //<debug error>
109529 if (unit !== lengthUnit) {
109530 Ext.Logger.error("Length unit: '" + unit + "' in value: '" + value + "' of property: '" + name + "' is not " +
109531 "valid for animation. Only 'px' is allowed");
109532 }
109533 //</debug>
109534 }
109535 else {
109536 return value + lengthUnit;
109537 }
109538 }
109539
109540 return value;
109541 }
109542 else if (type == 'number') {
109543 if (value == 0) {
109544 return '0';
109545 }
109546
109547 if (this.lengthProperties[name]) {
109548 return value + lengthUnit;
109549 }
109550
109551 if (this.angleProperties[name]) {
109552 return value + this.DEFAULT_UNIT_ANGLE;
109553 }
109554
109555 if (this.durationProperties[name]) {
109556 return value + this.DEFAULT_UNIT_DURATION;
109557 }
109558 }
109559 else if (name === 'transform') {
109560 transformMethods = this.transformMethods;
109561 transformValues = [];
109562
109563 for (i = 0,ln = transformMethods.length; i < ln; i++) {
109564 method = transformMethods[i];
109565
109566 transformValues.push(method + '(' + this.formatValue(value[method], method) + ')');
109567 }
109568
109569 return transformValues.join(' ');
109570 }
109571 else if (Ext.isArray(value)) {
109572 values = [];
109573
109574 for (i = 0,ln = value.length; i < ln; i++) {
109575 values.push(this.formatValue(value[i], name));
109576 }
109577
109578 return (values.length > 0) ? values.join(', ') : 'none';
109579 }
109580
109581 return value;
109582 }
109583 });
109584
109585 /**
109586 * @author Jacky Nguyen <jacky@sencha.com>
109587 * @private
109588 */
109589 Ext.define('Ext.fx.runner.CssTransition', {
109590 extend: Ext.fx.runner.Css ,
109591
109592
109593 listenersAttached: false,
109594
109595 constructor: function() {
109596 this.runningAnimationsData = {};
109597
109598 return this.callParent(arguments);
109599 },
109600
109601 attachListeners: function() {
109602 this.listenersAttached = true;
109603 this.getEventDispatcher().addListener('element', '*', 'transitionend', 'onTransitionEnd', this);
109604 },
109605
109606 onTransitionEnd: function(e) {
109607 var target = e.target,
109608 id = target.id;
109609
109610 if (id && this.runningAnimationsData.hasOwnProperty(id)) {
109611 this.refreshRunningAnimationsData(Ext.get(target), [e.browserEvent.propertyName]);
109612 }
109613 },
109614
109615 onAnimationEnd: function(element, data, animation, isInterrupted, isReplaced) {
109616 var id = element.getId(),
109617 runningData = this.runningAnimationsData[id],
109618 endRules = {},
109619 endData = {},
109620 runningNameMap, toPropertyNames, i, ln, name;
109621
109622 animation.un('stop', 'onAnimationStop', this);
109623
109624 if (runningData) {
109625 runningNameMap = runningData.nameMap;
109626 }
109627
109628 endRules[id] = endData;
109629
109630 if (data.onBeforeEnd) {
109631 data.onBeforeEnd.call(data.scope || this, element, isInterrupted);
109632 }
109633
109634 animation.fireEvent('animationbeforeend', animation, element, isInterrupted);
109635 this.fireEvent('animationbeforeend', this, animation, element, isInterrupted);
109636
109637 if (isReplaced || (!isInterrupted && !data.preserveEndState)) {
109638 toPropertyNames = data.toPropertyNames;
109639
109640 for (i = 0,ln = toPropertyNames.length; i < ln; i++) {
109641 name = toPropertyNames[i];
109642
109643 if (runningNameMap && !runningNameMap.hasOwnProperty(name)) {
109644 endData[name] = null;
109645 }
109646 }
109647 }
109648
109649 if (data.after) {
109650 Ext.merge(endData, data.after);
109651 }
109652
109653 this.applyStyles(endRules);
109654
109655 if (data.onEnd) {
109656 data.onEnd.call(data.scope || this, element, isInterrupted);
109657 }
109658
109659 animation.fireEvent('animationend', animation, element, isInterrupted);
109660 this.fireEvent('animationend', this, animation, element, isInterrupted);
109661 Ext.AnimationQueue.stop(Ext.emptyFn, animation);
109662 },
109663
109664 onAllAnimationsEnd: function(element) {
109665 var id = element.getId(),
109666 endRules = {};
109667
109668 delete this.runningAnimationsData[id];
109669
109670 endRules[id] = {
109671 'transition-property': null,
109672 'transition-duration': null,
109673 'transition-timing-function': null,
109674 'transition-delay': null
109675 };
109676
109677 this.applyStyles(endRules);
109678 this.fireEvent('animationallend', this, element);
109679 },
109680
109681 hasRunningAnimations: function(element) {
109682 var id = element.getId(),
109683 runningAnimationsData = this.runningAnimationsData;
109684
109685 return runningAnimationsData.hasOwnProperty(id) && runningAnimationsData[id].sessions.length > 0;
109686 },
109687
109688 refreshRunningAnimationsData: function(element, propertyNames, interrupt, replace) {
109689 var id = element.getId(),
109690 runningAnimationsData = this.runningAnimationsData,
109691 runningData = runningAnimationsData[id];
109692
109693 if (!runningData) {
109694 return;
109695 }
109696
109697 var nameMap = runningData.nameMap,
109698 nameList = runningData.nameList,
109699 sessions = runningData.sessions,
109700 ln, j, subLn, name,
109701 i, session, map, list,
109702 hasCompletedSession = false;
109703
109704 interrupt = Boolean(interrupt);
109705 replace = Boolean(replace);
109706
109707 if (!sessions) {
109708 return this;
109709 }
109710
109711 ln = sessions.length;
109712
109713 if (ln === 0) {
109714 return this;
109715 }
109716
109717 if (replace) {
109718 runningData.nameMap = {};
109719 nameList.length = 0;
109720
109721 for (i = 0; i < ln; i++) {
109722 session = sessions[i];
109723 this.onAnimationEnd(element, session.data, session.animation, interrupt, replace);
109724 }
109725
109726 sessions.length = 0;
109727 }
109728 else {
109729 for (i = 0; i < ln; i++) {
109730 session = sessions[i];
109731 map = session.map;
109732 list = session.list;
109733
109734 for (j = 0,subLn = propertyNames.length; j < subLn; j++) {
109735 name = propertyNames[j];
109736
109737 if (map[name]) {
109738 delete map[name];
109739 Ext.Array.remove(list, name);
109740 session.length--;
109741 if (--nameMap[name] == 0) {
109742 delete nameMap[name];
109743 Ext.Array.remove(nameList, name);
109744 }
109745 }
109746 }
109747
109748 if (session.length == 0) {
109749 sessions.splice(i, 1);
109750 i--;
109751 ln--;
109752
109753 hasCompletedSession = true;
109754 this.onAnimationEnd(element, session.data, session.animation, interrupt);
109755 }
109756 }
109757 }
109758
109759 if (!replace && !interrupt && sessions.length == 0 && hasCompletedSession) {
109760 this.onAllAnimationsEnd(element);
109761 }
109762 },
109763
109764 getRunningData: function(id) {
109765 var runningAnimationsData = this.runningAnimationsData;
109766
109767 if (!runningAnimationsData.hasOwnProperty(id)) {
109768 runningAnimationsData[id] = {
109769 nameMap: {},
109770 nameList: [],
109771 sessions: []
109772 };
109773 }
109774
109775 return runningAnimationsData[id];
109776 },
109777
109778 getTestElement: function() {
109779 var testElement = this.testElement,
109780 iframe, iframeDocument, iframeStyle;
109781
109782 if (!testElement) {
109783 iframe = document.createElement('iframe');
109784 iframeStyle = iframe.style;
109785 iframeStyle.setProperty('visibility', 'hidden', 'important');
109786 iframeStyle.setProperty('width', '0px', 'important');
109787 iframeStyle.setProperty('height', '0px', 'important');
109788 iframeStyle.setProperty('position', 'absolute', 'important');
109789 iframeStyle.setProperty('border', '0px', 'important');
109790 iframeStyle.setProperty('zIndex', '-1000', 'important');
109791
109792 document.body.appendChild(iframe);
109793 iframeDocument = iframe.contentDocument;
109794
109795 iframeDocument.open();
109796 iframeDocument.writeln('</body>');
109797 iframeDocument.close();
109798
109799 this.testElement = testElement = iframeDocument.createElement('div');
109800 testElement.style.setProperty('position', 'absolute', 'important');
109801 iframeDocument.body.appendChild(testElement);
109802 this.testElementComputedStyle = window.getComputedStyle(testElement);
109803 }
109804
109805 return testElement;
109806 },
109807
109808 getCssStyleValue: function(name, value) {
109809 var testElement = this.getTestElement(),
109810 computedStyle = this.testElementComputedStyle,
109811 style = testElement.style;
109812
109813 style.setProperty(name, value);
109814
109815 if (Ext.browser.is.Firefox) {
109816 // We force a repaint of the element in Firefox to make sure the computedStyle to be updated
109817 testElement.offsetHeight;
109818 }
109819
109820 value = computedStyle.getPropertyValue(name);
109821 style.removeProperty(name);
109822
109823 return value;
109824 },
109825
109826 run: function(animations) {
109827 var me = this,
109828 isLengthPropertyMap = this.lengthProperties,
109829 fromData = {},
109830 toData = {},
109831 data = {},
109832 element, elementId, from, to, before,
109833 fromPropertyNames, toPropertyNames,
109834 doApplyTo, message,
109835 runningData, elementData,
109836 i, j, ln, animation, propertiesLength, sessionNameMap,
109837 computedStyle, formattedName, name, toFormattedValue,
109838 computedValue, fromFormattedValue, isLengthProperty,
109839 runningNameMap, runningNameList, runningSessions, runningSession;
109840
109841 if (!this.listenersAttached) {
109842 this.attachListeners();
109843 }
109844
109845 animations = Ext.Array.from(animations);
109846
109847 for (i = 0,ln = animations.length; i < ln; i++) {
109848 animation = animations[i];
109849 animation = Ext.factory(animation, Ext.fx.Animation);
109850 element = animation.getElement();
109851
109852 // Empty function to prevent idleTasks from running while we animate.
109853 Ext.AnimationQueue.start(Ext.emptyFn, animation);
109854
109855 computedStyle = window.getComputedStyle(element.dom);
109856
109857 elementId = element.getId();
109858
109859 data = Ext.merge({}, animation.getData());
109860
109861 if (animation.onBeforeStart) {
109862 animation.onBeforeStart.call(animation.scope || this, element);
109863 }
109864 animation.fireEvent('animationstart', animation);
109865 this.fireEvent('animationstart', this, animation);
109866
109867 data[elementId] = data;
109868
109869 before = data.before;
109870 from = data.from;
109871 to = data.to;
109872
109873 data.fromPropertyNames = fromPropertyNames = [];
109874 data.toPropertyNames = toPropertyNames = [];
109875
109876 for (name in to) {
109877 if (to.hasOwnProperty(name)) {
109878 to[name] = toFormattedValue = this.formatValue(to[name], name);
109879 formattedName = this.formatName(name);
109880 isLengthProperty = isLengthPropertyMap.hasOwnProperty(name);
109881
109882 if (!isLengthProperty) {
109883 toFormattedValue = this.getCssStyleValue(formattedName, toFormattedValue);
109884 }
109885
109886 if (from.hasOwnProperty(name)) {
109887 from[name] = fromFormattedValue = this.formatValue(from[name], name);
109888
109889 if (!isLengthProperty) {
109890 fromFormattedValue = this.getCssStyleValue(formattedName, fromFormattedValue);
109891 }
109892
109893 if (toFormattedValue !== fromFormattedValue) {
109894 fromPropertyNames.push(formattedName);
109895 toPropertyNames.push(formattedName);
109896 }
109897 }
109898 else {
109899 computedValue = computedStyle.getPropertyValue(formattedName);
109900
109901 if (toFormattedValue !== computedValue) {
109902 toPropertyNames.push(formattedName);
109903 }
109904 }
109905 }
109906 }
109907
109908 propertiesLength = toPropertyNames.length;
109909
109910 if (propertiesLength === 0) {
109911 this.onAnimationEnd(element, data, animation);
109912 continue;
109913 }
109914
109915 runningData = this.getRunningData(elementId);
109916 runningSessions = runningData.sessions;
109917
109918 if (runningSessions.length > 0) {
109919 this.refreshRunningAnimationsData(
109920 element, Ext.Array.merge(fromPropertyNames, toPropertyNames), true, data.replacePrevious
109921 );
109922 }
109923
109924 runningNameMap = runningData.nameMap;
109925 runningNameList = runningData.nameList;
109926
109927 sessionNameMap = {};
109928 for (j = 0; j < propertiesLength; j++) {
109929 name = toPropertyNames[j];
109930 sessionNameMap[name] = true;
109931
109932 if (!runningNameMap.hasOwnProperty(name)) {
109933 runningNameMap[name] = 1;
109934 runningNameList.push(name);
109935 }
109936 else {
109937 runningNameMap[name]++;
109938 }
109939 }
109940
109941 runningSession = {
109942 element: element,
109943 map: sessionNameMap,
109944 list: toPropertyNames.slice(),
109945 length: propertiesLength,
109946 data: data,
109947 animation: animation
109948 };
109949 runningSessions.push(runningSession);
109950
109951 animation.on('stop', 'onAnimationStop', this);
109952
109953 elementData = Ext.apply({}, before);
109954 Ext.apply(elementData, from);
109955
109956 if (runningNameList.length > 0) {
109957 fromPropertyNames = Ext.Array.difference(runningNameList, fromPropertyNames);
109958 toPropertyNames = Ext.Array.merge(fromPropertyNames, toPropertyNames);
109959 elementData['transition-property'] = fromPropertyNames;
109960 }
109961
109962 fromData[elementId] = elementData;
109963 toData[elementId] = Ext.apply({}, to);
109964
109965 toData[elementId]['transition-property'] = toPropertyNames;
109966 toData[elementId]['transition-duration'] = data.duration;
109967 toData[elementId]['transition-timing-function'] = data.easing;
109968 toData[elementId]['transition-delay'] = data.delay;
109969
109970 animation.startTime = Date.now();
109971 }
109972
109973 message = this.$className;
109974
109975 this.applyStyles(fromData);
109976
109977 doApplyTo = function(e) {
109978 if (e.data === message && e.source === window) {
109979 window.removeEventListener('message', doApplyTo, false);
109980 me.applyStyles(toData);
109981 }
109982 };
109983
109984 if(Ext.browser.is.IE) {
109985 window.requestAnimationFrame(function() {
109986 window.addEventListener('message', doApplyTo, false);
109987 window.postMessage(message, '*');
109988 });
109989 }else{
109990 window.addEventListener('message', doApplyTo, false);
109991 window.postMessage(message, '*');
109992 }
109993 },
109994
109995 onAnimationStop: function(animation) {
109996 var runningAnimationsData = this.runningAnimationsData,
109997 id, runningData, sessions, i, ln, session;
109998
109999 for (id in runningAnimationsData) {
110000 if (runningAnimationsData.hasOwnProperty(id)) {
110001 runningData = runningAnimationsData[id];
110002 sessions = runningData.sessions;
110003
110004 for (i = 0,ln = sessions.length; i < ln; i++) {
110005 session = sessions[i];
110006 if (session.animation === animation) {
110007 this.refreshRunningAnimationsData(session.element, session.list.slice(), false);
110008 }
110009 }
110010 }
110011 }
110012 }
110013 });
110014
110015 /**
110016 * @class Ext.fx.Runner
110017 * @private
110018 */
110019 Ext.define('Ext.fx.Runner', {
110020
110021
110022
110023
110024
110025 constructor: function() {
110026 return new Ext.fx.runner.CssTransition();
110027 }
110028 });
110029
110030 /**
110031 * @private
110032 */
110033 Ext.define('Ext.fx.animation.Cube', {
110034 extend: Ext.fx.animation.Abstract ,
110035
110036 alias: 'animation.cube',
110037
110038 config: {
110039 /**
110040 * @cfg
110041 * @inheritdoc
110042 */
110043 before: {
110044 // 'transform-style': 'preserve-3d'
110045 },
110046
110047 after: {},
110048
110049 /**
110050 * @cfg {String} direction The direction of which the slide animates
110051 * @accessor
110052 */
110053 direction: 'right',
110054
110055 out: false
110056 },
110057
110058 // getData: function() {
110059 // var to = this.getTo(),
110060 // from = this.getFrom(),
110061 // out = this.getOut(),
110062 // direction = this.getDirection(),
110063 // el = this.getElement(),
110064 // elW = el.getWidth(),
110065 // elH = el.getHeight(),
110066 // halfWidth = (elW / 2),
110067 // halfHeight = (elH / 2),
110068 // fromTransform = {},
110069 // toTransform = {},
110070 // originalFromTransform = {
110071 // rotateY: 0,
110072 // translateX: 0,
110073 // translateZ: 0
110074 // },
110075 // originalToTransform = {
110076 // rotateY: 90,
110077 // translateX: halfWidth,
110078 // translateZ: halfWidth
110079 // },
110080 // originalVerticalFromTransform = {
110081 // rotateX: 0,
110082 // translateY: 0,
110083 // translateZ: 0
110084 // },
110085 // originalVerticalToTransform = {
110086 // rotateX: 90,
110087 // translateY: halfHeight,
110088 // translateZ: halfHeight
110089 // },
110090 // tempTransform;
110091 //
110092 // if (direction == "left" || direction == "right") {
110093 // if (out) {
110094 // toTransform = originalToTransform;
110095 // fromTransform = originalFromTransform;
110096 // } else {
110097 // toTransform = originalFromTransform;
110098 // fromTransform = originalToTransform;
110099 // fromTransform.rotateY *= -1;
110100 // fromTransform.translateX *= -1;
110101 // }
110102 //
110103 // if (direction === 'right') {
110104 // tempTransform = fromTransform;
110105 // fromTransform = toTransform;
110106 // toTransform = tempTransform;
110107 // }
110108 // }
110109 //
110110 // if (direction == "up" || direction == "down") {
110111 // if (out) {
110112 // toTransform = originalVerticalFromTransform;
110113 // fromTransform = {
110114 // rotateX: -90,
110115 // translateY: halfHeight,
110116 // translateZ: halfHeight
110117 // };
110118 // } else {
110119 // fromTransform = originalVerticalFromTransform;
110120 // toTransform = {
110121 // rotateX: 90,
110122 // translateY: -halfHeight,
110123 // translateZ: halfHeight
110124 // };
110125 // }
110126 //
110127 // if (direction == "up") {
110128 // tempTransform = fromTransform;
110129 // fromTransform = toTransform;
110130 // toTransform = tempTransform;
110131 // }
110132 // }
110133 //
110134 // from.set('transform', fromTransform);
110135 // to.set('transform', toTransform);
110136 //
110137 // return this.callParent(arguments);
110138 // },
110139
110140 getData: function() {
110141 var to = this.getTo(),
110142 from = this.getFrom(),
110143 before = this.getBefore(),
110144 after = this.getAfter(),
110145 out = this.getOut(),
110146 direction = this.getDirection(),
110147 el = this.getElement(),
110148 elW = el.getWidth(),
110149 elH = el.getHeight(),
110150 origin = out ? '100% 100%' : '0% 0%',
110151 fromOpacity = 1,
110152 toOpacity = 1,
110153 transformFrom = {
110154 rotateY: 0,
110155 translateZ: 0
110156 },
110157 transformTo = {
110158 rotateY: 0,
110159 translateZ: 0
110160 };
110161
110162 if (direction == "left" || direction == "right") {
110163 if (out) {
110164 toOpacity = 0.5;
110165 transformTo.translateZ = elW;
110166 transformTo.rotateY = -90;
110167 } else {
110168 fromOpacity = 0.5;
110169 transformFrom.translateZ = elW;
110170 transformFrom.rotateY = 90;
110171 }
110172 }
110173
110174 before['transform-origin'] = origin;
110175 after['transform-origin'] = null;
110176
110177 to.set('transform', transformTo);
110178 from.set('transform', transformFrom);
110179
110180 from.set('opacity', fromOpacity);
110181 to.set('opacity', toOpacity);
110182
110183 return this.callParent(arguments);
110184 }
110185 });
110186
110187 /**
110188 * @private
110189 */
110190 Ext.define('Ext.fx.animation.Wipe', {
110191 extend: Ext.fx.Animation ,
110192 alternateClassName: 'Ext.fx.animation.WipeIn',
110193
110194 config: {
110195 /**
110196 * Valid values are 'ease', 'linear', ease-in', 'ease-out', 'ease-in-out',
110197 * or a cubic-bezier curve as defined by CSS.
110198 */
110199 easing: 'ease-out',
110200
110201 /**
110202 * @cfg {String} direction The direction of which the slide animates
110203 * @accessor
110204 */
110205 direction: 'right',
110206
110207 /**
110208 * @cfg {Boolean} out True if you want to make this animation wipe out, instead of slide in.
110209 * @accessor
110210 */
110211 out: false
110212 },
110213
110214 refresh: function() {
110215 var me = this,
110216 el = me.getElement(),
110217 elBox = el.dom.getBoundingClientRect(),
110218 elWidth = elBox.width,
110219 elHeight = elBox.height,
110220 from = me.getFrom(),
110221 to = me.getTo(),
110222 out = me.getOut(),
110223 direction = me.getDirection(),
110224 maskFromX = 0,
110225 maskFromY = 0,
110226 maskToX = 0,
110227 maskToY = 0,
110228 mask, tmp;
110229
110230 switch (direction) {
110231 case 'up':
110232 if (out) {
110233 mask = '-webkit-gradient(linear, left top, left bottom, from(#000), to(transparent), color-stop(33%, #000), color-stop(66%, transparent))';
110234 maskFromY = elHeight * 3 + 'px';
110235 maskToY = elHeight + 'px';
110236 } else {
110237 mask = '-webkit-gradient(linear, left top, left bottom, from(transparent), to(#000), color-stop(66%, #000), color-stop(33%, transparent))';
110238 maskFromY = -elHeight * 2 + 'px';
110239 maskToY = 0;
110240 }
110241
110242 break;
110243
110244 case 'down':
110245 if (out) {
110246 mask = '-webkit-gradient(linear, left top, left bottom, from(transparent), to(#000), color-stop(66%, #000), color-stop(33%, transparent))';
110247 maskFromY = -elHeight * 2 + 'px';
110248 maskToY = 0;
110249 } else {
110250 mask = '-webkit-gradient(linear, left top, left bottom, from(#000), to(transparent), color-stop(33%, #000), color-stop(66%, transparent))';
110251 maskFromY = elHeight * 3 + 'px';
110252 maskToY = elHeight + 'px';
110253 }
110254
110255 break;
110256
110257 case 'right':
110258 if (out) {
110259 mask = '-webkit-gradient(linear, right top, left top, from(#000), to(transparent), color-stop(33%, #000), color-stop(66%, transparent))';
110260 maskFromX = -elWidth * 2 + 'px';
110261 maskToX = 0;
110262 } else {
110263 mask = '-webkit-gradient(linear, right top, left top, from(transparent), to(#000), color-stop(66%, #000), color-stop(33%, transparent))';
110264 maskToX = -elWidth * 2 + 'px';
110265 }
110266
110267 break;
110268
110269 case 'left':
110270 if (out) {
110271 mask = '-webkit-gradient(linear, right top, left top, from(transparent), to(#000), color-stop(66%, #000), color-stop(33%, transparent))';
110272 maskToX = -elWidth * 2 + 'px';
110273 } else {
110274 mask = '-webkit-gradient(linear, right top, left top, from(#000), to(transparent), color-stop(33%, #000), color-stop(66%, transparent))';
110275 maskFromX = -elWidth * 2 + 'px';
110276 maskToX = 0;
110277 }
110278
110279 break;
110280 }
110281
110282 if (!out) {
110283 tmp = maskFromY;
110284 maskFromY = maskToY;
110285 maskToY = tmp;
110286
110287 tmp = maskFromX;
110288 maskFromX = maskToX;
110289 maskToX = tmp;
110290 }
110291
110292 from.set('mask-image', mask);
110293 from.set('mask-size', elWidth * 3 + 'px ' + elHeight * 3 + 'px');
110294 from.set('mask-position-x', maskFromX);
110295 from.set('mask-position-y', maskFromY);
110296
110297 to.set('mask-position-x', maskToX);
110298 to.set('mask-position-y', maskToY);
110299
110300 // me.setEasing(out ? 'ease-in' : 'ease-out');
110301 },
110302
110303 getData: function() {
110304 this.refresh();
110305
110306 return this.callParent(arguments);
110307 }
110308 });
110309
110310 /**
110311 * @private
110312 */
110313 Ext.define('Ext.fx.animation.WipeOut', {
110314 extend: Ext.fx.animation.Wipe ,
110315
110316 config: {
110317 // @hide
110318 out: true
110319 }
110320 });
110321
110322 /**
110323 * @private
110324 */
110325 Ext.define('Ext.fx.easing.EaseIn', {
110326 extend: Ext.fx.easing.Linear ,
110327
110328 alias: 'easing.ease-in',
110329
110330 config: {
110331 exponent: 4,
110332 duration: 1500
110333 },
110334
110335 getValue: function() {
110336 var deltaTime = Ext.Date.now() - this.getStartTime(),
110337 duration = this.getDuration(),
110338 startValue = this.getStartValue(),
110339 endValue = this.getEndValue(),
110340 distance = this.distance,
110341 theta = deltaTime / duration,
110342 thetaEnd = Math.pow(theta, this.getExponent()),
110343 currentValue = startValue + (thetaEnd * distance);
110344
110345 if (deltaTime >= duration) {
110346 this.isEnded = true;
110347 return endValue;
110348 }
110349
110350 return currentValue;
110351 }
110352 });
110353
110354 /**
110355 * @private
110356 */
110357 Ext.define('Ext.fx.layout.card.Cube', {
110358 extend: Ext.fx.layout.card.Style ,
110359
110360 alias: 'fx.layout.card.cube',
110361
110362 config: {
110363 reverse: null,
110364 inAnimation: {
110365 type: 'cube'
110366 },
110367 outAnimation: {
110368 type: 'cube',
110369 out: true
110370 }
110371 }
110372 });
110373
110374 /**
110375 * @private
110376 */
110377 Ext.define('Ext.fx.layout.card.ScrollCover', {
110378 extend: Ext.fx.layout.card.Scroll ,
110379
110380 alias: 'fx.layout.card.scrollcover',
110381
110382 onActiveItemChange: function(cardLayout, inItem, outItem, options, controller) {
110383 var containerElement, containerSize, xy, animConfig,
110384 inTranslate, outTranslate;
110385
110386 this.lastController = controller;
110387 this.inItem = inItem;
110388
110389 if (inItem && outItem) {
110390 containerElement = this.getLayout().container.innerElement;
110391
110392 containerSize = containerElement.getSize();
110393 xy = this.calculateXY(containerSize);
110394 animConfig = {
110395 easing: this.getEasing(),
110396 duration: this.getDuration()
110397 };
110398
110399 inItem.renderElement.dom.style.setProperty('visibility', 'hidden', 'important');
110400 inTranslate = inItem.setTranslatable(true).getTranslatable();
110401 outTranslate = outItem.setTranslatable(true).getTranslatable();
110402
110403 outTranslate.translate({ x: 0, y: 0});
110404 // outItem.setTranslate(null);
110405 inTranslate.translate({ x: xy.left, y: xy.top});
110406 inTranslate.getWrapper().dom.style.setProperty('z-index', '100', 'important');
110407 inItem.show();
110408
110409 inTranslate.on({
110410 animationstart: 'onInAnimationStart',
110411 animationend: 'onInAnimationEnd',
110412 scope: this
110413 });
110414 inTranslate.translateAnimated({ x: 0, y: 0}, animConfig);
110415
110416 controller.pause();
110417 }
110418 },
110419
110420 onInAnimationStart: function() {
110421 this.inItem.renderElement.dom.style.removeProperty('visibility');
110422 },
110423
110424 onInAnimationEnd: function() {
110425 this.inItem.getTranslatable().getWrapper().dom.style.removeProperty('z-index'); // Remove this when we can remove translatable
110426 // this.inItem.setTranslatable(null);
110427 this.lastController.resume();
110428 }
110429 });
110430
110431 /**
110432 * @private
110433 */
110434 Ext.define('Ext.fx.layout.card.ScrollReveal', {
110435 extend: Ext.fx.layout.card.Scroll ,
110436
110437 alias: 'fx.layout.card.scrollreveal',
110438
110439 onActiveItemChange: function(cardLayout, inItem, outItem, options, controller) {
110440 var containerElement, containerSize, xy, animConfig,
110441 outTranslate, inTranslate;
110442
110443 this.lastController = controller;
110444 this.outItem = outItem;
110445 this.inItem = inItem;
110446
110447 if (inItem && outItem) {
110448 containerElement = this.getLayout().container.innerElement;
110449
110450 containerSize = containerElement.getSize();
110451 xy = this.calculateXY(containerSize);
110452 animConfig = {
110453 easing: this.getEasing(),
110454 duration: this.getDuration()
110455 };
110456
110457 outTranslate = outItem.setTranslatable(true).getTranslatable();
110458 inTranslate = inItem.setTranslatable(true).getTranslatable();
110459 outTranslate.getWrapper().dom.style.setProperty('z-index', '100', 'important');
110460 outTranslate.translate({ x: 0, y: 0});
110461 inTranslate.translate({ x: 0, y: 0});
110462
110463 inItem.show();
110464
110465 outTranslate.on({
110466 animationend: 'onOutAnimationEnd',
110467 scope: this
110468 });
110469
110470 outTranslate.translateAnimated({ x: xy.x, y: xy.y}, animConfig);
110471
110472 controller.pause();
110473 }
110474 },
110475
110476 onOutAnimationEnd: function() {
110477 this.outItem.getTranslatable().getWrapper().dom.style.removeProperty('z-index'); // Remove this when we can remove translatable
110478 // this.outItem.setTranslatable(null);
110479 this.lastController.resume();
110480 }
110481 });
110482
110483 /**
110484 * @author Jacky Nguyen <jacky@sencha.com>
110485 * @private
110486 */
110487 Ext.define('Ext.fx.runner.CssAnimation', {
110488 extend: Ext.fx.runner.Css ,
110489
110490 constructor: function() {
110491 this.runningAnimationsMap = {};
110492
110493 this.elementEndStates = {};
110494
110495 this.animationElementMap = {};
110496
110497 this.keyframesRulesCache = {};
110498
110499 this.uniqueId = 0;
110500
110501 return this.callParent(arguments);
110502 },
110503
110504 attachListeners: function() {
110505 var eventDispatcher = this.getEventDispatcher();
110506
110507 this.listenersAttached = true;
110508
110509 eventDispatcher.addListener('element', '*', 'animationstart', 'onAnimationStart', this);
110510 eventDispatcher.addListener('element', '*', 'animationend', 'onAnimationEnd', this);
110511 },
110512
110513 onAnimationStart: function(e) {
110514 var name = e.browserEvent.animationName,
110515 elementId = this.animationElementMap[name],
110516 animation = this.runningAnimationsMap[elementId][name],
110517 elementEndStates = this.elementEndStates,
110518 elementEndState = elementEndStates[elementId],
110519 data = {};
110520
110521 console.log("START============= " + name);
110522 if (elementEndState) {
110523 delete elementEndStates[elementId];
110524
110525 data[elementId] = elementEndState;
110526
110527 this.applyStyles(data);
110528 }
110529
110530 if (animation.before) {
110531 data[elementId] = animation.before;
110532
110533 this.applyStyles(data);
110534 }
110535 },
110536
110537 onAnimationEnd: function(e) {
110538 var element = e.target,
110539 name = e.browserEvent.animationName,
110540 animationElementMap = this.animationElementMap,
110541 elementId = animationElementMap[name],
110542 runningAnimationsMap = this.runningAnimationsMap,
110543 runningAnimations = runningAnimationsMap[elementId],
110544 animation = runningAnimations[name];
110545
110546 console.log("END============= " + name);
110547
110548 if (animation.onBeforeEnd) {
110549 animation.onBeforeEnd.call(animation.scope || this, element);
110550 }
110551
110552 if (animation.onEnd) {
110553 animation.onEnd.call(animation.scope || this, element);
110554 }
110555
110556 delete animationElementMap[name];
110557 delete runningAnimations[name];
110558
110559 this.removeKeyframesRule(name);
110560 },
110561
110562 generateAnimationId: function() {
110563 return 'animation-' + (++this.uniqueId);
110564 },
110565
110566 run: function(animations) {
110567 var data = {},
110568 elementEndStates = this.elementEndStates,
110569 animationElementMap = this.animationElementMap,
110570 runningAnimationsMap = this.runningAnimationsMap,
110571 runningAnimations, states,
110572 elementId, animationId, i, ln, animation,
110573 name, runningAnimation,
110574 names, durations, easings, delays, directions, iterations;
110575
110576 if (!this.listenersAttached) {
110577 this.attachListeners();
110578 }
110579
110580 animations = Ext.Array.from(animations);
110581
110582 for (i = 0,ln = animations.length; i < ln; i++) {
110583 animation = animations[i];
110584
110585 animation = Ext.factory(animation, Ext.fx.Animation);
110586 elementId = animation.getElement().getId();
110587 animationId = animation.getName() || this.generateAnimationId();
110588
110589 animationElementMap[animationId] = elementId;
110590
110591 animation = animation.getData();
110592 states = animation.states;
110593
110594 this.addKeyframesRule(animationId, states);
110595
110596 runningAnimations = runningAnimationsMap[elementId];
110597
110598 if (!runningAnimations) {
110599 runningAnimations = runningAnimationsMap[elementId] = {};
110600 }
110601
110602 runningAnimations[animationId] = animation;
110603
110604 names = [];
110605 durations = [];
110606 easings = [];
110607 delays = [];
110608 directions = [];
110609 iterations = [];
110610
110611 for (name in runningAnimations) {
110612 if (runningAnimations.hasOwnProperty(name)) {
110613 runningAnimation = runningAnimations[name];
110614
110615 names.push(name);
110616 durations.push(runningAnimation.duration);
110617 easings.push(runningAnimation.easing);
110618 delays.push(runningAnimation.delay);
110619 directions.push(runningAnimation.direction);
110620 iterations.push(runningAnimation.iteration);
110621 }
110622 }
110623
110624 data[elementId] = {
110625 'animation-name' : names,
110626 'animation-duration' : durations,
110627 'animation-timing-function' : easings,
110628 'animation-delay' : delays,
110629 'animation-direction' : directions,
110630 'animation-iteration-count' : iterations
110631 };
110632
110633 // Ext.apply(data[elementId], animation.origin);
110634
110635 if (animation.preserveEndState) {
110636 elementEndStates[elementId] = states['100%'];
110637 }
110638 }
110639
110640 this.applyStyles(data);
110641 },
110642
110643 addKeyframesRule: function(name, keyframes) {
110644 var percentage, properties,
110645 keyframesRule,
110646 styleSheet, rules, styles, rulesLength, key, value;
110647
110648 styleSheet = this.getStyleSheet();
110649 rules = styleSheet.cssRules;
110650 rulesLength = rules.length;
110651 styleSheet.insertRule('@' + this.vendorPrefix + 'keyframes ' + name + '{}', rulesLength);
110652
110653 keyframesRule = rules[rulesLength];
110654
110655 for (percentage in keyframes) {
110656 properties = keyframes[percentage];
110657
110658 rules = keyframesRule.cssRules;
110659 rulesLength = rules.length;
110660
110661 styles = [];
110662
110663 for (key in properties) {
110664 value = this.formatValue(properties[key], key);
110665 key = this.formatName(key);
110666
110667 styles.push(key + ':' + value);
110668 }
110669
110670 keyframesRule.insertRule(percentage + '{' + styles.join(';') + '}', rulesLength);
110671 }
110672
110673 return this;
110674 },
110675
110676 removeKeyframesRule: function(name) {
110677 var styleSheet = this.getStyleSheet(),
110678 rules = styleSheet.cssRules,
110679 i, ln, rule;
110680
110681 for (i = 0,ln = rules.length; i < ln; i++) {
110682 rule = rules[i];
110683
110684 if (rule.name === name) {
110685 styleSheet.removeRule(i);
110686 break;
110687 }
110688 }
110689
110690 return this;
110691 }
110692 });
110693
110694 //<feature logger>
110695 Ext.define('Ext.log.Base', {
110696 config: {},
110697
110698 constructor: function(config) {
110699 this.initConfig(config);
110700
110701 return this;
110702 }
110703 });
110704 //</feature>
110705
110706 //<feature logger>
110707 /**
110708 * @class Ext.Logger
110709 * Logs messages to help with debugging.
110710 *
110711 * ## Example
110712 *
110713 * Ext.Logger.deprecate('This method is no longer supported.');
110714 *
110715 * @singleton
110716 */
110717 (function() {
110718 var Logger = Ext.define('Ext.log.Logger', {
110719
110720 extend: Ext.log.Base ,
110721
110722 statics: {
110723 defaultPriority: 'info',
110724
110725 priorities: {
110726 /**
110727 * @method verbose
110728 * Convenience method for {@link #log} with priority 'verbose'.
110729 */
110730 verbose: 0,
110731 /**
110732 * @method info
110733 * Convenience method for {@link #log} with priority 'info'.
110734 */
110735 info: 1,
110736 /**
110737 * @method deprecate
110738 * Convenience method for {@link #log} with priority 'deprecate'.
110739 */
110740 deprecate: 2,
110741 /**
110742 * @method warn
110743 * Convenience method for {@link #log} with priority 'warn'.
110744 */
110745 warn: 3,
110746 /**
110747 * @method error
110748 * Convenience method for {@link #log} with priority 'error'.
110749 */
110750 error: 4
110751 }
110752 },
110753
110754 config: {
110755 enabled: true,
110756 minPriority: 'deprecate',
110757 writers: {}
110758 },
110759
110760 /**
110761 * Logs a message to help with debugging.
110762 * @param {String} message Message to log.
110763 * @param {Number} priority Priority of the log message.
110764 */
110765 log: function(message, priority, callerId) {
110766 if (!this.getEnabled()) {
110767 return this;
110768 }
110769
110770 var statics = Logger,
110771 priorities = statics.priorities,
110772 priorityValue = priorities[priority],
110773 caller = this.log.caller,
110774 callerDisplayName = '',
110775 writers = this.getWriters(),
110776 event, i, originalCaller;
110777
110778 if (!priority) {
110779 priority = 'info';
110780 }
110781
110782 if (priorities[this.getMinPriority()] > priorityValue) {
110783 return this;
110784 }
110785
110786 if (!callerId) {
110787 callerId = 1;
110788 }
110789
110790 if (Ext.isArray(message)) {
110791 message = message.join(" ");
110792 }
110793 else {
110794 message = String(message);
110795 }
110796
110797 if (typeof callerId == 'number') {
110798 i = callerId;
110799
110800 do {
110801 i--;
110802
110803 caller = caller.caller;
110804
110805 if (!caller) {
110806 break;
110807 }
110808
110809 if (!originalCaller) {
110810 originalCaller = caller.caller;
110811 }
110812
110813 if (i <= 0 && caller.displayName) {
110814 break;
110815 }
110816 }
110817 while (caller !== originalCaller);
110818
110819 callerDisplayName = Ext.getDisplayName(caller);
110820 }
110821 else {
110822 caller = caller.caller;
110823 callerDisplayName = Ext.getDisplayName(callerId) + '#' + caller.$name;
110824 }
110825
110826 event = {
110827 time: Ext.Date.now(),
110828 priority: priorityValue,
110829 priorityName: priority,
110830 message: message,
110831 caller: caller,
110832 callerDisplayName: callerDisplayName
110833 };
110834
110835 for (i in writers) {
110836 if (writers.hasOwnProperty(i)) {
110837 writers[i].write(Ext.merge({}, event));
110838 }
110839 }
110840
110841 return this;
110842 }
110843
110844 }, function() {
110845 Ext.Object.each(this.priorities, function(priority) {
110846 this.override(priority, function(message, callerId) {
110847 if (!callerId) {
110848 callerId = 1;
110849 }
110850
110851 if (typeof callerId == 'number') {
110852 callerId += 1;
110853 }
110854
110855 this.log(message, priority, callerId);
110856 });
110857 }, this);
110858 });
110859
110860 })();
110861 //</feature>
110862
110863 //<feature logger>
110864 Ext.define('Ext.log.filter.Filter', {
110865 extend: Ext.log.Base ,
110866
110867 accept: function(event) {
110868 return true;
110869 }
110870 });
110871 //</feature>
110872
110873 //<feature logger>
110874 Ext.define('Ext.log.filter.Priority', {
110875 extend: Ext.log.filter.Filter ,
110876
110877 config: {
110878 minPriority: 1
110879 },
110880
110881 accept: function(event) {
110882 return event.priority >= this.getMinPriority();
110883 }
110884 });
110885 //</feature>
110886
110887 //<feature logger>
110888 Ext.define('Ext.log.formatter.Formatter', {
110889 extend: Ext.log.Base ,
110890
110891 config: {
110892 messageFormat: "{message}"
110893 },
110894
110895 format: function(event) {
110896 return this.substitute(this.getMessageFormat(), event);
110897 },
110898
110899 substitute: function(template, data) {
110900 var name, value;
110901
110902 for (name in data) {
110903 if (data.hasOwnProperty(name)) {
110904 value = data[name];
110905
110906 template = template.replace(new RegExp("\\{" + name + "\\}", "g"), value);
110907 }
110908 }
110909
110910 return template;
110911 }
110912 });
110913 //</feature>
110914
110915 //<feature logger>
110916 Ext.define('Ext.log.formatter.Default', {
110917 extend: Ext.log.formatter.Formatter ,
110918
110919 config: {
110920 messageFormat: "[{priorityName}][{callerDisplayName}] {message}"
110921 },
110922
110923 format: function(event) {
110924 var event = Ext.merge({}, event, {
110925 priorityName: event.priorityName.toUpperCase()
110926 });
110927
110928 return this.callParent([event]);
110929 }
110930 });
110931 //</feature>
110932
110933 //<feature logger>
110934 Ext.define('Ext.log.formatter.Identity', {
110935 extend: Ext.log.formatter.Default ,
110936
110937 config: {
110938 messageFormat: "[{osIdentity}][{browserIdentity}][{timestamp}][{priorityName}][{callerDisplayName}] {message}"
110939 },
110940
110941 format: function(event) {
110942 event.timestamp = Ext.Date.toString();
110943 event.browserIdentity = Ext.browser.name + ' ' + Ext.browser.version;
110944 event.osIdentity = Ext.os.name + ' ' + Ext.os.version;
110945
110946 return this.callParent(arguments);
110947 }
110948 });
110949 //</feature>
110950
110951 //<feature logger>
110952 Ext.define('Ext.log.writer.Writer', {
110953 extend: Ext.log.Base ,
110954
110955
110956
110957 config: {
110958 formatter: null,
110959 filters: {}
110960 },
110961
110962 constructor: function() {
110963 this.activeFilters = [];
110964
110965 return this.callParent(arguments);
110966 },
110967
110968 updateFilters: function(filters) {
110969 var activeFilters = this.activeFilters,
110970 i, filter;
110971
110972 activeFilters.length = 0;
110973
110974 for (i in filters) {
110975 if (filters.hasOwnProperty(i)) {
110976 filter = filters[i];
110977 activeFilters.push(filter);
110978 }
110979 }
110980 },
110981
110982 write: function(event) {
110983 var filters = this.activeFilters,
110984 formatter = this.getFormatter(),
110985 i, ln, filter;
110986
110987 for (i = 0,ln = filters.length; i < ln; i++) {
110988 filter = filters[i];
110989
110990 if (!filters[i].accept(event)) {
110991 return this;
110992 }
110993 }
110994
110995 if (formatter) {
110996 event.message = formatter.format(event);
110997 }
110998
110999 this.doWrite(event);
111000
111001 return this;
111002 },
111003
111004 // @private
111005 doWrite: Ext.emptyFn
111006 });
111007 //</feature>
111008
111009 //<feature logger>
111010 Ext.define('Ext.log.writer.Console', {
111011
111012 extend: Ext.log.writer.Writer ,
111013
111014 config: {
111015 throwOnErrors: true,
111016 throwOnWarnings: false
111017 },
111018
111019 doWrite: function(event) {
111020 var message = event.message,
111021 priority = event.priorityName,
111022 consoleMethod;
111023
111024 if (priority === 'error' && this.getThrowOnErrors()) {
111025 throw new Error(message);
111026 }
111027
111028 if (typeof console !== 'undefined') {
111029 consoleMethod = priority;
111030
111031 if (consoleMethod === 'deprecate') {
111032 consoleMethod = 'warn';
111033 }
111034
111035 if (consoleMethod === 'warn' && this.getThrowOnWarnings()) {
111036 throw new Error(message);
111037 }
111038
111039 if (!(consoleMethod in console)) {
111040 consoleMethod = 'log';
111041 }
111042
111043 console[consoleMethod](message);
111044 }
111045 }
111046 });
111047 //</feature>
111048
111049 //<feature logger>
111050 Ext.define('Ext.log.writer.DocumentTitle', {
111051
111052 extend: Ext.log.writer.Writer ,
111053
111054 doWrite: function(event) {
111055 var message = event.message;
111056
111057 document.title = message;
111058 }
111059 });
111060 //</feature>
111061
111062 //<feature logger>
111063 Ext.define('Ext.log.writer.Remote', {
111064 extend: Ext.log.writer.Writer ,
111065
111066
111067
111068
111069
111070 config: {
111071 batchSendDelay: 100,
111072 onFailureRetryDelay: 500,
111073 url: ''
111074 },
111075
111076 isSending: false,
111077
111078 sendingTimer: null,
111079
111080 constructor: function() {
111081 this.queue = [];
111082
111083 this.send = Ext.Function.bind(this.send, this);
111084
111085 return this.callParent(arguments);
111086 },
111087
111088 doWrite: function(event) {
111089 var queue = this.queue;
111090 queue.push(event.message);
111091
111092 if (!this.isSending && this.sendingTimer === null) {
111093 this.sendingTimer = setTimeout(this.send, this.getBatchSendDelay());
111094 }
111095 },
111096
111097 send: function() {
111098 var queue = this.queue,
111099 messages = queue.slice();
111100
111101 queue.length = 0;
111102
111103 this.sendingTimer = null;
111104
111105 if (messages.length > 0) {
111106 this.doSend(messages);
111107 }
111108 },
111109
111110 doSend: function(messages) {
111111 var me = this;
111112
111113 me.isSending = true;
111114
111115 Ext.Ajax.request({
111116 url: me.getUrl(),
111117 method: 'POST',
111118 params: {
111119 messages: messages.join("\n")
111120 },
111121 success: function(){
111122 me.isSending = false;
111123 me.send();
111124 },
111125 failure: function() {
111126 setTimeout(function() {
111127 me.doSend(messages);
111128 }, me.getOnFailureRetryDelay());
111129 },
111130 scope: me
111131 });
111132 }
111133 });
111134 //</feature>
111135
111136 /**
111137 * This component is used in {@link Ext.navigation.View} to control animations in the toolbar. You should never need to
111138 * interact with the component directly, unless you are subclassing it.
111139 * @private
111140 * @author Robert Dougan <rob@sencha.com>
111141 */
111142 Ext.define('Ext.navigation.Bar', {
111143 extend: Ext.TitleBar ,
111144
111145
111146
111147
111148
111149
111150 // @private
111151 isToolbar: true,
111152
111153 config: {
111154 /**
111155 * @cfg
111156 * @inheritdoc
111157 */
111158 baseCls: Ext.baseCSSPrefix + 'toolbar',
111159
111160 /**
111161 * @cfg
111162 * @inheritdoc
111163 */
111164 cls: Ext.baseCSSPrefix + 'navigation-bar',
111165
111166 /**
111167 * @cfg {String} ui
111168 * Style options for Toolbar. Either 'light' or 'dark'.
111169 * @accessor
111170 */
111171 ui: 'dark',
111172
111173 /**
111174 * @cfg {String} title
111175 * The title of the toolbar. You should NEVER set this, it is used internally. You set the title of the
111176 * navigation bar by giving a navigation views children a title configuration.
111177 * @private
111178 * @accessor
111179 */
111180 title: null,
111181
111182 /**
111183 * @cfg
111184 * @hide
111185 * @accessor
111186 */
111187 defaultType: 'button',
111188
111189 /**
111190 * @cfg
111191 * @ignore
111192 * @accessor
111193 */
111194 layout: {
111195 type: 'hbox'
111196 },
111197
111198 /**
111199 * @cfg {Array/Object} items The child items to add to this NavigationBar. The {@link #cfg-defaultType} of
111200 * a NavigationBar is {@link Ext.Button}, so you do not need to specify an `xtype` if you are adding
111201 * buttons.
111202 *
111203 * You can also give items a `align` configuration which will align the item to the `left` or `right` of
111204 * the NavigationBar.
111205 * @hide
111206 * @accessor
111207 */
111208
111209 /**
111210 * @cfg {String} defaultBackButtonText
111211 * The text to be displayed on the back button if:
111212 * a) The previous view does not have a title
111213 * b) The {@link #useTitleForBackButtonText} configuration is true.
111214 * @private
111215 * @accessor
111216 */
111217 defaultBackButtonText: 'Back',
111218
111219 /**
111220 * @cfg {Object} animation
111221 * @private
111222 * @accessor
111223 */
111224 animation: {
111225 duration: 300
111226 },
111227
111228 /**
111229 * @cfg {Boolean} useTitleForBackButtonText
111230 * Set to false if you always want to display the {@link #defaultBackButtonText} as the text
111231 * on the back button. True if you want to use the previous views title.
111232 * @private
111233 * @accessor
111234 */
111235 useTitleForBackButtonText: null,
111236
111237 /**
111238 * @cfg {Ext.navigation.View} view A reference to the navigation view this bar is linked to.
111239 * @private
111240 * @accessor
111241 */
111242 view: null,
111243
111244 /**
111245 * @cfg {Boolean} androidAnimation Optionally enable CSS transforms on Android 2
111246 * for NavigationBar animations. Note that this may cause flickering if the
111247 * NavigationBar is hidden.
111248 * @accessor
111249 */
111250 android2Transforms: false,
111251
111252 /**
111253 * @cfg {Ext.Button/Object} backButton The configuration for the back button
111254 * @private
111255 * @accessor
111256 */
111257 backButton: {
111258 align: 'left',
111259 ui: 'back',
111260 hidden: true
111261 }
111262 },
111263
111264 platformConfig: [{
111265 theme: ['Blackberry', 'Blackberry103'],
111266 animation: false
111267 }],
111268
111269 /**
111270 * @event back
111271 * Fires when the back button was tapped.
111272 * @param {Ext.navigation.Bar} this This bar
111273 */
111274
111275 constructor: function(config) {
111276 config = config || {};
111277
111278 if (!config.items) {
111279 config.items = [];
111280 }
111281
111282 this.backButtonStack = [];
111283 this.activeAnimations = [];
111284
111285 this.callParent([config]);
111286 },
111287
111288 /**
111289 * @private
111290 */
111291 applyBackButton: function(config) {
111292 return Ext.factory(config, Ext.Button, this.getBackButton());
111293 },
111294
111295 /**
111296 * @private
111297 */
111298 updateBackButton: function(newBackButton, oldBackButton) {
111299 if (oldBackButton) {
111300 this.remove(oldBackButton);
111301 }
111302
111303 if (newBackButton) {
111304 this.add(newBackButton);
111305
111306 newBackButton.on({
111307 scope: this,
111308 tap: this.onBackButtonTap
111309 });
111310 }
111311 },
111312
111313 onBackButtonTap: function() {
111314 this.fireEvent('back', this);
111315 },
111316
111317 /**
111318 * @private
111319 */
111320 updateView: function(newView) {
111321 var me = this,
111322 backButton = me.getBackButton(),
111323 innerItems, i, backButtonText, item, title, titleText;
111324
111325 me.getItems();
111326
111327 if (newView) {
111328 //update the back button stack with the current inner items of the view
111329 innerItems = newView.getInnerItems();
111330 for (i = 0; i < innerItems.length; i++) {
111331 item = innerItems[i];
111332 title = (item.getTitle) ? item.getTitle() : item.config.title;
111333
111334 me.backButtonStack.push(title || '&nbsp;');
111335 }
111336
111337 titleText = me.getTitleText();
111338
111339 if (titleText === undefined) {
111340 titleText = '';
111341 }
111342
111343 me.setTitle(titleText);
111344
111345 backButtonText = me.getBackButtonText();
111346 if (backButtonText) {
111347 backButton.setText(backButtonText);
111348 backButton.show();
111349 }
111350 }
111351 },
111352
111353 /**
111354 * @private
111355 */
111356 onViewAdd: function(view, item) {
111357 var me = this,
111358 backButtonStack = me.backButtonStack,
111359 hasPrevious, title;
111360
111361 me.endAnimation();
111362
111363 title = (item.getTitle) ? item.getTitle() : item.config.title;
111364
111365 backButtonStack.push(title || '&nbsp;');
111366 hasPrevious = backButtonStack.length > 1;
111367
111368 me.doChangeView(view, hasPrevious, false);
111369 },
111370
111371 /**
111372 * @private
111373 */
111374 onViewRemove: function(view) {
111375 var me = this,
111376 backButtonStack = me.backButtonStack,
111377 hasPrevious;
111378
111379 me.endAnimation();
111380 backButtonStack.pop();
111381 hasPrevious = backButtonStack.length > 1;
111382
111383 me.doChangeView(view, hasPrevious, true);
111384 },
111385
111386 /**
111387 * @private
111388 */
111389 doChangeView: function(view, hasPrevious, reverse) {
111390 var me = this,
111391 leftBox = me.leftBox,
111392 leftBoxElement = leftBox.element,
111393 titleComponent = me.titleComponent,
111394 titleElement = titleComponent.element,
111395 backButton = me.getBackButton(),
111396 titleText = me.getTitleText(),
111397 backButtonText = me.getBackButtonText(),
111398 animation = me.getAnimation() && view.getLayout().getAnimation(),
111399 animated = animation && animation.isAnimation && view.isPainted(),
111400 properties, leftGhost, titleGhost, leftProps, titleProps;
111401
111402 if (animated) {
111403 leftGhost = me.createProxy(leftBox.element);
111404 leftBoxElement.setStyle('opacity', '0');
111405 backButton.setText(backButtonText);
111406 backButton[hasPrevious ? 'show' : 'hide']();
111407
111408 titleGhost = me.createProxy(titleComponent.element.getParent());
111409 titleElement.setStyle('opacity', '0');
111410 me.setTitle(titleText);
111411
111412 properties = me.measureView(leftGhost, titleGhost, reverse);
111413 leftProps = properties.left;
111414 titleProps = properties.title;
111415
111416 me.isAnimating = true;
111417 me.animate(leftBoxElement, leftProps.element);
111418 me.animate(titleElement, titleProps.element, function() {
111419 titleElement.setLeft(properties.titleLeft);
111420 me.isAnimating = false;
111421 me.refreshTitlePosition();
111422 });
111423
111424 if (Ext.browser.is.AndroidStock2 && !this.getAndroid2Transforms()) {
111425 leftGhost.ghost.destroy();
111426 titleGhost.ghost.destroy();
111427 }
111428 else {
111429 me.animate(leftGhost.ghost, leftProps.ghost);
111430 me.animate(titleGhost.ghost, titleProps.ghost, function() {
111431 leftGhost.ghost.destroy();
111432 titleGhost.ghost.destroy();
111433 });
111434 }
111435 }
111436 else {
111437 if (hasPrevious) {
111438 backButton.setText(backButtonText);
111439 backButton.show();
111440 }
111441 else {
111442 backButton.hide();
111443 }
111444 me.setTitle(titleText);
111445 }
111446 },
111447
111448 /**
111449 * Calculates and returns the position values needed for the back button when you are pushing a title.
111450 * @private
111451 */
111452 measureView: function(oldLeft, oldTitle, reverse) {
111453 var me = this,
111454 barElement = me.element,
111455 newLeftElement = me.leftBox.element,
111456 titleElement = me.titleComponent.element,
111457 minOffset = Math.min(barElement.getWidth() / 3, 200),
111458 newLeftWidth = newLeftElement.getWidth(),
111459 barX = barElement.getX(),
111460 barWidth = barElement.getWidth(),
111461 titleX = titleElement.getX(),
111462 titleLeft = titleElement.getLeft(),
111463 titleWidth = titleElement.getWidth(),
111464 oldLeftX = oldLeft.x,
111465 oldLeftWidth = oldLeft.width,
111466 oldLeftLeft = oldLeft.left,
111467 useLeft = Ext.browser.is.AndroidStock2 && !this.getAndroid2Transforms(),
111468 newOffset, oldOffset, leftAnims, titleAnims, omega, theta;
111469
111470 theta = barX - oldLeftX - oldLeftWidth;
111471 if (reverse) {
111472 newOffset = theta;
111473 oldOffset = Math.min(titleX - oldLeftWidth, minOffset);
111474 }
111475 else {
111476 oldOffset = theta;
111477 newOffset = Math.min(titleX - barX, minOffset);
111478 }
111479
111480 if (useLeft) {
111481 leftAnims = {
111482 element: {
111483 from: {
111484 left: newOffset,
111485 opacity: 1
111486 },
111487 to: {
111488 left: 0,
111489 opacity: 1
111490 }
111491 }
111492 };
111493 }
111494 else {
111495 leftAnims = {
111496 element: {
111497 from: {
111498 transform: {
111499 translateX: newOffset
111500 },
111501 opacity: 0
111502 },
111503 to: {
111504 transform: {
111505 translateX: 0
111506 },
111507 opacity: 1
111508 }
111509 },
111510 ghost: {
111511 to: {
111512 transform: {
111513 translateX: oldOffset
111514 },
111515 opacity: 0
111516 }
111517 }
111518 };
111519 }
111520
111521 theta = barX - titleX + newLeftWidth;
111522 if ((oldLeftLeft + titleWidth) > titleX) {
111523 omega = barX - titleX - titleWidth;
111524 }
111525
111526 if (reverse) {
111527 titleElement.setLeft(0);
111528
111529 oldOffset = barX + barWidth - titleX - titleWidth;
111530
111531 if (omega !== undefined) {
111532 newOffset = omega;
111533 }
111534 else {
111535 newOffset = theta;
111536 }
111537 }
111538 else {
111539 newOffset = barX + barWidth - titleX - titleWidth;
111540
111541 if (omega !== undefined) {
111542 oldOffset = omega;
111543 }
111544 else {
111545 oldOffset = theta;
111546 }
111547
111548 newOffset = Math.max(titleLeft, newOffset);
111549 }
111550
111551 if (useLeft) {
111552 titleAnims = {
111553 element: {
111554 from: {
111555 left: newOffset,
111556 opacity: 1
111557 },
111558 to: {
111559 left: titleLeft,
111560 opacity: 1
111561 }
111562 }
111563 };
111564 }
111565 else {
111566 titleAnims = {
111567 element: {
111568 from: {
111569 transform: {
111570 translateX: newOffset
111571 },
111572 opacity: 0
111573 },
111574 to: {
111575 transform: {
111576 translateX: titleLeft
111577 },
111578 opacity: 1
111579 }
111580 },
111581 ghost: {
111582 to: {
111583 transform: {
111584 translateX: oldOffset
111585 },
111586 opacity: 0
111587 }
111588 }
111589 };
111590 }
111591
111592 return {
111593 left: leftAnims,
111594 title: titleAnims,
111595 titleLeft: titleLeft
111596 };
111597 },
111598
111599 /**
111600 * Helper method used to animate elements.
111601 * You pass it an element, objects for the from and to positions an option onEnd callback called when the animation is over.
111602 * Normally this method is passed configurations returned from the methods such as #measureTitle(true) etc.
111603 * It is called from the #pushLeftBoxAnimated, #pushTitleAnimated, #popBackButtonAnimated and #popTitleAnimated
111604 * methods.
111605 *
111606 * If the current device is Android, it will use top/left to animate.
111607 * If it is anything else, it will use transform.
111608 * @private
111609 */
111610 animate: function(element, config, callback) {
111611 var me = this,
111612 animation;
111613
111614 //reset the left of the element
111615 element.setLeft(0);
111616
111617 config = Ext.apply(config, {
111618 element: element,
111619 easing: 'ease-in-out',
111620 duration: me.getAnimation().duration || 250,
111621 preserveEndState: true
111622 });
111623
111624 animation = new Ext.fx.Animation(config);
111625 animation.on('animationend', function() {
111626 if (callback) {
111627 callback.call(me);
111628 }
111629 }, me);
111630
111631 Ext.Animator.run(animation);
111632 me.activeAnimations.push(animation);
111633 },
111634
111635 endAnimation: function() {
111636 var activeAnimations = this.activeAnimations,
111637 animation, i, ln;
111638
111639 if (activeAnimations) {
111640 ln = activeAnimations.length;
111641 for (i = 0; i < ln; i++) {
111642 animation = activeAnimations[i];
111643 if (animation.isAnimating) {
111644 animation.stopAnimation();
111645 }
111646 else {
111647 animation.destroy();
111648 }
111649 }
111650 this.activeAnimations = [];
111651 }
111652 },
111653
111654 refreshTitlePosition: function() {
111655 if (!this.isAnimating) {
111656 this.callParent();
111657 }
111658 },
111659
111660 /**
111661 * Returns the text needed for the current back button at anytime.
111662 * @private
111663 */
111664 getBackButtonText: function() {
111665 var text = this.backButtonStack[this.backButtonStack.length - 2],
111666 useTitleForBackButtonText = this.getUseTitleForBackButtonText();
111667
111668 if (!useTitleForBackButtonText) {
111669 if (text) {
111670 text = this.getDefaultBackButtonText();
111671 }
111672 }
111673
111674 return text;
111675 },
111676
111677 /**
111678 * Returns the text needed for the current title at anytime.
111679 * @private
111680 */
111681 getTitleText: function() {
111682 return this.backButtonStack[this.backButtonStack.length - 1];
111683 },
111684
111685 /**
111686 * Handles removing back button stacks from this bar
111687 * @private
111688 */
111689 beforePop: function(count) {
111690 count--;
111691 for (var i = 0; i < count; i++) {
111692 this.backButtonStack.pop();
111693 }
111694 },
111695
111696 /**
111697 * We override the hidden method because we don't want to remove it from the view using display:none. Instead we just position it off
111698 * the screen, much like the navigation bar proxy. This means that all animations, pushing, popping etc. all still work when if you hide/show
111699 * this bar at any time.
111700 * @private
111701 */
111702 doSetHidden: function(hidden) {
111703 if (!hidden) {
111704 this.element.setStyle({
111705 position: 'relative',
111706 top: 'auto',
111707 left: 'auto',
111708 width: 'auto'
111709 });
111710 } else {
111711 this.element.setStyle({
111712 position: 'absolute',
111713 top: '-1000px',
111714 left: '-1000px',
111715 width: this.element.getWidth() + 'px'
111716 });
111717 }
111718 },
111719
111720 /**
111721 * Creates a proxy element of the passed element, and positions it in the same position, using absolute positioning.
111722 * The createNavigationBarProxy method uses this to create proxies of the backButton and the title elements.
111723 * @private
111724 */
111725 createProxy: function(element) {
111726 var ghost, x, y, left, width;
111727
111728 ghost = element.dom.cloneNode(true);
111729 ghost.id = element.id + '-proxy';
111730
111731 //insert it into the toolbar
111732 element.getParent().dom.appendChild(ghost);
111733
111734 //set the x/y
111735 ghost = Ext.get(ghost);
111736 x = element.getX();
111737 y = element.getY();
111738 left = element.getLeft();
111739 width = element.getWidth();
111740 ghost.setStyle('position', 'absolute');
111741 ghost.setX(x);
111742 ghost.setY(y);
111743 ghost.setHeight(element.getHeight());
111744 ghost.setWidth(width);
111745
111746 return {
111747 x: x,
111748 y: y,
111749 left: left,
111750 width: width,
111751 ghost: ghost
111752 };
111753 }
111754 });
111755
111756 /**
111757 * @author Robert Dougan <rob@sencha.com>
111758 *
111759 * NavigationView is basically a {@link Ext.Container} with a {@link Ext.layout.Card card} layout, so only one view
111760 * can be visible at a time. However, NavigationView also adds extra functionality on top of this to allow
111761 * you to `push` and `pop` views at any time. When you do this, your NavigationView will automatically animate
111762 * between your current active view, and the new view you want to `push`, or the previous view you want to `pop`.
111763 *
111764 * Using the NavigationView is very simple. Here is a basic example of it in action:
111765 *
111766 * @example
111767 * var view = Ext.create('Ext.NavigationView', {
111768 * fullscreen: true,
111769 *
111770 * items: [{
111771 * title: 'First',
111772 * items: [{
111773 * xtype: 'button',
111774 * text: 'Push a new view!',
111775 * handler: function() {
111776 * // use the push() method to push another view. It works much like
111777 * // add() or setActiveItem(). it accepts a view instance, or you can give it
111778 * // a view config.
111779 * view.push({
111780 * title: 'Second',
111781 * html: 'Second view!'
111782 * });
111783 * }
111784 * }]
111785 * }]
111786 * });
111787 *
111788 * Now, here comes the fun part: you can push any view/item into the NavigationView, at any time, and it will
111789 * automatically handle the animations between the two views, including adding a back button (if necessary)
111790 * and showing the new title.
111791 *
111792 * view.push({
111793 * title: 'A new view',
111794 * html: 'Some new content'
111795 * });
111796 *
111797 * As you can see, it is as simple as calling the {@link #method-push} method, with a new view (instance or object). Done.
111798 *
111799 * You can also `pop` a view at any time. This will remove the top-most view from the NavigationView, and animate back
111800 * to the previous view. You can do this using the {@link #method-pop} method (which requires no arguments).
111801 *
111802 * view.pop();
111803 *
111804 * Applications that need compatibility with ##Older Android## devices will want to see the {@link #layout} config for details on
111805 * disabling navigation view animations as these devices have poor animation support and performance.
111806 *
111807 * ###Further Reading
111808 * [Sencha Touch Navigation View Guide](../../../components/navigation_view.html)
111809 */
111810 Ext.define('Ext.navigation.View', {
111811 extend: Ext.Container ,
111812 alternateClassName: 'Ext.NavigationView',
111813 xtype: 'navigationview',
111814
111815
111816 config: {
111817 /**
111818 * @cfg
111819 * @inheritdoc
111820 */
111821 baseCls: Ext.baseCSSPrefix + 'navigationview',
111822
111823 /**
111824 * @cfg {Boolean/Object} navigationBar
111825 * The NavigationBar used in this navigation view. It defaults to be docked to the top.
111826 *
111827 * You can just pass in a normal object if you want to customize the NavigationBar. For example:
111828 *
111829 * navigationBar: {
111830 * ui: 'dark',
111831 * docked: 'bottom'
111832 * }
111833 *
111834 * You **cannot** specify a *title* property in this configuration. The title of the navigationBar is taken
111835 * from the configuration of this views children:
111836 *
111837 * view.push({
111838 * title: 'This views title which will be shown in the navigation bar',
111839 * html: 'Some HTML'
111840 * });
111841 *
111842 * @accessor
111843 */
111844 navigationBar: {
111845 docked: 'top'
111846 },
111847
111848 /**
111849 * @cfg {String} defaultBackButtonText
111850 * The text to be displayed on the back button if:
111851 *
111852 * - The previous view does not have a title.
111853 * - The {@link #useTitleForBackButtonText} configuration is `true`.
111854 * @accessor
111855 */
111856 defaultBackButtonText: 'Back',
111857
111858 /**
111859 * @cfg {Boolean} useTitleForBackButtonText
111860 * Set to `false` if you always want to display the {@link #defaultBackButtonText} as the text
111861 * on the back button. `true` if you want to use the previous views title.
111862 * @accessor
111863 */
111864 useTitleForBackButtonText: false,
111865
111866 /**
111867 * @cfg {Array/Object} items The child items to add to this NavigationView. This is usually an array of Component
111868 * configurations or instances, for example:
111869 *
111870 * Ext.create('Ext.Container', {
111871 * items: [
111872 * {
111873 * xtype: 'panel',
111874 * title: 'My title',
111875 * html: 'This is an item'
111876 * }
111877 * ]
111878 * });
111879 *
111880 * If you want a title to be displayed in the {@link #navigationBar}, you must specify a `title` configuration in your
111881 * view, like above.
111882 *
111883 * __Note:__ Only one view will be visible at a time. If you want to change to another view, use the {@link #method-push} or
111884 * {@link #setActiveItem} methods.
111885 * @accessor
111886 */
111887
111888 /**
111889 * @cfg {Object}
111890 * Layout used in this navigation view, type must be set to 'card'.
111891 * **Android NOTE:** Older Android devices have poor animation performance. It is recommended to set the animation to null, for example:
111892 *
111893 * layout: {
111894 * type: 'card',
111895 * animation: null
111896 * }
111897 *
111898 * @accessor
111899 */
111900 layout: {
111901 type: 'card',
111902 animation: {
111903 duration: 300,
111904 easing: 'ease-out',
111905 type: 'slide',
111906 direction: 'left'
111907 }
111908 }
111909 },
111910
111911 /**
111912 * @event push
111913 * Fires when a view is pushed into this navigation view
111914 * @param {Ext.navigation.View} this The component instance
111915 * @param {Mixed} view The view that has been pushed
111916 */
111917
111918 /**
111919 * @event pop
111920 * Fires when a view is popped from this navigation view
111921 * @param {Ext.navigation.View} this The component instance
111922 * @param {Mixed} view The view that has been popped
111923 */
111924
111925 /**
111926 * @event back
111927 * Fires when the back button in the navigation view was tapped.
111928 * @param {Ext.navigation.View} this The component instance\
111929 */
111930
111931 platformConfig: [{
111932 theme: ['Blackberry', 'Blackberry103'],
111933 navigationBar: {
111934 splitNavigation: true
111935 }
111936 }],
111937
111938 // @private
111939 initialize: function() {
111940 var me = this,
111941 navBar = me.getNavigationBar();
111942
111943 //add a listener onto the back button in the navigationbar
111944 if (navBar) {
111945 navBar.on({
111946 back: me.onBackButtonTap,
111947 scope: me
111948 });
111949
111950 me.relayEvents(navBar, 'rightbuttontap');
111951
111952 me.relayEvents(me, {
111953 add: 'push',
111954 remove: 'pop'
111955 });
111956 }
111957
111958 //<debug>
111959 var layout = me.getLayout();
111960 if (layout && !layout.isCard) {
111961 Ext.Logger.error('The base layout for a NavigationView must always be a Card Layout');
111962 }
111963 //</debug>
111964 },
111965
111966 /**
111967 * @private
111968 */
111969 applyLayout: function(config) {
111970 config = config || {};
111971
111972 return config;
111973 },
111974
111975 /**
111976 * @private
111977 * Called when the user taps on the back button
111978 */
111979 onBackButtonTap: function() {
111980 this.pop();
111981 this.fireEvent('back', this);
111982 },
111983
111984 /**
111985 * Pushes a new view into this navigation view using the default animation that this view has.
111986 * @param {Object} view The view to push.
111987 * @return {Ext.Component} The new item you just pushed.
111988 */
111989 push: function(view) {
111990 return this.add(view);
111991 },
111992
111993 /**
111994 * Removes the current active view from the stack and sets the previous view using the default animation
111995 * of this view. You can also pass a {@link Ext.ComponentQuery} selector to target what inner item to pop to.
111996 * @param {Number/String/Object} count If a Number, the number of views you want to pop. If a String, the pops to a matching
111997 * component query. If an Object, the pops to a matching view instance.
111998 * @return {Ext.Component} The new active item
111999 */
112000 pop: function(count) {
112001 if (this.beforePop(count)) {
112002 return this.doPop();
112003 }
112004 },
112005
112006 /**
112007 * @private
112008 * Calculates whether it needs to remove any items from the stack when you are popping more than 1
112009 * item. If it does, it removes those views from the stack and returns `true`.
112010 * @return {Boolean} `true` if it has removed views.
112011 */
112012 beforePop: function(count) {
112013 var me = this,
112014 innerItems = me.getInnerItems();
112015
112016 if (Ext.isString(count) || Ext.isObject(count)) {
112017 var last = innerItems.length - 1,
112018 i;
112019
112020 for (i = last; i >= 0; i--) {
112021 if ((Ext.isString(count) && Ext.ComponentQuery.is(innerItems[i], count)) || (Ext.isObject(count) && count == innerItems[i])) {
112022 count = last - i;
112023 break;
112024 }
112025 }
112026
112027 if (!Ext.isNumber(count)) {
112028 return false;
112029 }
112030 }
112031
112032 var ln = innerItems.length,
112033 toRemove;
112034
112035 //default to 1 pop
112036 if (!Ext.isNumber(count) || count < 1) {
112037 count = 1;
112038 }
112039
112040 //check if we are trying to remove more items than we have
112041 count = Math.min(count, ln - 1);
112042
112043 if (count) {
112044 //we need to reset the backButtonStack in the navigation bar
112045 me.getNavigationBar().beforePop(count);
112046
112047 //get the items we need to remove from the view and remove theme
112048 toRemove = innerItems.splice(-count, count - 1);
112049 for (i = 0; i < toRemove.length; i++) {
112050 this.remove(toRemove[i]);
112051 }
112052
112053 return true;
112054 }
112055
112056 return false;
112057 },
112058
112059 /**
112060 * @private
112061 */
112062 doPop: function() {
112063 var me = this,
112064 innerItems = this.getInnerItems();
112065
112066 //set the new active item to be the new last item of the stack
112067 me.remove(innerItems[innerItems.length - 1]);
112068
112069 // Hide the backButton
112070 if (innerItems.length < 3 && this.$backButton) {
112071 this.$backButton.hide();
112072 }
112073
112074 // Update the title container
112075 if (this.$titleContainer) {
112076 //<debug>
112077 if (!this.$titleContainer.setTitle) {
112078 Ext.Logger.error('You have selected to display a title in a component that does not \
112079 support titles in NavigationView. Please remove the `title` configuration from your \
112080 NavigationView item, or change it to a component that has a `setTitle` method.');
112081 }
112082 //</debug>
112083
112084 var item = innerItems[innerItems.length - 2];
112085 this.$titleContainer.setTitle((item.getTitle) ? item.getTitle() : item.config.title);
112086 }
112087
112088 return this.getActiveItem();
112089 },
112090
112091 /**
112092 * Returns the previous item, if one exists.
112093 * @return {Mixed} The previous view
112094 */
112095 getPreviousItem: function() {
112096 var innerItems = this.getInnerItems();
112097 return innerItems[innerItems.length - 2];
112098 },
112099
112100 /**
112101 * Updates the backbutton text accordingly in the {@link #navigationBar}
112102 * @private
112103 */
112104 updateUseTitleForBackButtonText: function(useTitleForBackButtonText) {
112105 var navigationBar = this.getNavigationBar();
112106 if (navigationBar) {
112107 navigationBar.setUseTitleForBackButtonText(useTitleForBackButtonText);
112108 }
112109 },
112110
112111 /**
112112 * Updates the backbutton text accordingly in the {@link #navigationBar}
112113 * @private
112114 */
112115 updateDefaultBackButtonText: function(defaultBackButtonText) {
112116 var navigationBar = this.getNavigationBar();
112117 if (navigationBar) {
112118 navigationBar.setDefaultBackButtonText(defaultBackButtonText);
112119 }
112120 },
112121
112122 /**
112123 * This is called when an Item is added to the BackButtonContainer of a SplitNavigation View
112124 * @private
112125 *
112126 * @param toolbar
112127 * @param item
112128 */
112129 onBackButtonContainerAdd: function(toolbar, item) {
112130 item.on({
112131 scope: this,
112132 show: this.refreshBackButtonContainer,
112133 hide: this.refreshBackButtonContainer
112134 });
112135 this.refreshBackButtonContainer();
112136 },
112137
112138 /**
112139 * This is called when an Item is removed from the BackButtonContainer of a SplitNavigation View
112140 * @private
112141 *
112142 * @param toolbar
112143 * @param item
112144 */
112145 onBackButtonContainerRemove: function(toolbar, item) {
112146 item.un({
112147 scope: this,
112148 show: this.refreshBackButtonContainer,
112149 hide: this.refreshBackButtonContainer
112150 });
112151 this.refreshBackButtonContainer();
112152 },
112153
112154 /**
112155 * This is used for Blackberry SplitNavigation to monitor the state of child items in the bottom toolbar.
112156 * if no visible children exist the toolbar will be hidden
112157 * @private
112158 */
112159 refreshBackButtonContainer: function() {
112160 if (!this.$backButtonContainer) {
112161 return;
112162 }
112163 var i = 0,
112164 backButtonContainer = this.$backButtonContainer,
112165 items = backButtonContainer.items,
112166 item;
112167
112168 for(;i < items.length; i++) {
112169 item = items.get(i);
112170 if(!item.isHidden()) {
112171 this.$backButtonContainer.show();
112172 return;
112173 }
112174 }
112175
112176 this.$backButtonContainer.hide();
112177 },
112178
112179 // @private
112180 applyNavigationBar: function(config) {
112181 var me = this;
112182 if (!config) {
112183 config = {
112184 hidden: true,
112185 docked: 'top'
112186 };
112187 }
112188
112189 if (config.title) {
112190 delete config.title;
112191 //<debug>
112192 Ext.Logger.warn("Ext.navigation.View: The 'navigationBar' configuration does not accept a 'title' property. You " +
112193 "set the title of the navigationBar by giving this navigation view's children a 'title' property.");
112194 //</debug>
112195 }
112196
112197 config.view = this;
112198 config.useTitleForBackButtonText = this.getUseTitleForBackButtonText();
112199
112200 // Blackberry specific nav setup where title is on the top title bar and the bottom toolbar is used for buttons and BACK
112201 if (config.splitNavigation) {
112202 this.$titleContainer = this.add({
112203 docked: 'top',
112204 xtype: 'titlebar',
112205 ui: 'light',
112206 title: this.$currentTitle || ''
112207 });
112208
112209 var containerConfig = (config.splitNavigation === true) ? {} : config.splitNavigation;
112210
112211 this.$backButtonContainer = this.add({
112212 xtype: 'toolbar',
112213 docked: 'bottom',
112214 hidden: true
112215 });
112216
112217 // Any item that is added to the BackButtonContainer should be monitored for visibility
112218 // this will allow the toolbar to be hidden when no items exist in it.
112219 this.$backButtonContainer.on ({
112220 scope: me,
112221 add: me.onBackButtonContainerAdd,
112222 remove: me.onBackButtonContainerRemove
112223 });
112224
112225 this.$backButton = this.$backButtonContainer.add({
112226 xtype: 'button',
112227 text: 'Back',
112228 hidden: true,
112229 ui: 'back'
112230 });
112231
112232 // Default config items go into the bottom bar
112233 if(config.items) {
112234 this.$backButtonContainer.add(config.items);
112235 }
112236
112237 // If the user provided items and splitNav items, default items go into the bottom bar, split nav items go into the top
112238 if(containerConfig.items) {
112239 this.$titleContainer.add(containerConfig.items);
112240 }
112241
112242 this.$backButton.on({
112243 scope: this,
112244 tap: this.onBackButtonTap
112245 });
112246
112247 config = {
112248 hidden: true,
112249 docked: 'top'
112250 };
112251 }
112252
112253 return Ext.factory(config, Ext.navigation.Bar, this.getNavigationBar());
112254 },
112255
112256 // @private
112257 updateNavigationBar: function(newNavigationBar, oldNavigationBar) {
112258 if (oldNavigationBar) {
112259 this.remove(oldNavigationBar, true);
112260 }
112261
112262 if (newNavigationBar) {
112263 this.add(newNavigationBar);
112264 }
112265 },
112266
112267 /**
112268 * @private
112269 */
112270 applyActiveItem: function(activeItem, currentActiveItem) {
112271 var me = this,
112272 innerItems = me.getInnerItems();
112273
112274 // Make sure the items are already initialized
112275 me.getItems();
112276
112277 // If we are not initialzed yet, we should set the active item to the last item in the stack
112278 if (!me.initialized) {
112279 activeItem = innerItems.length - 1;
112280 }
112281
112282 return this.callParent([activeItem, currentActiveItem]);
112283 },
112284
112285 doResetActiveItem: function(innerIndex) {
112286 var me = this,
112287 innerItems = me.getInnerItems(),
112288 animation = me.getLayout().getAnimation();
112289
112290 if (innerIndex > 0) {
112291 if (animation && animation.isAnimation) {
112292 animation.setReverse(true);
112293 }
112294 me.setActiveItem(innerIndex - 1);
112295 me.getNavigationBar().onViewRemove(me, innerItems[innerIndex], innerIndex);
112296 }
112297 },
112298
112299 /**
112300 * @private
112301 */
112302 doRemove: function() {
112303 var animation = this.getLayout().getAnimation();
112304
112305 if (animation && animation.isAnimation) {
112306 animation.setReverse(false);
112307 }
112308
112309 this.callParent(arguments);
112310 },
112311
112312 /**
112313 * @private
112314 */
112315 onItemAdd: function(item, index) {
112316
112317 // Check for title configuration
112318 if (item && item.getDocked() && item.config.title === true) {
112319 this.$titleContainer = item;
112320 }
112321
112322 this.doItemLayoutAdd(item, index);
112323
112324 var navigaitonBar = this.getInitialConfig().navigationBar;
112325
112326 if (!this.isItemsInitializing && item.isInnerItem()) {
112327 this.setActiveItem(item);
112328
112329 // Update the navigationBar
112330 if (navigaitonBar) {
112331 this.getNavigationBar().onViewAdd(this, item, index);
112332 }
112333
112334 // Update the custom backButton
112335 if (this.$backButtonContainer) {
112336 this.$backButton.show();
112337 }
112338 }
112339
112340 if (item && item.isInnerItem()) {
112341 // Update the title container title
112342 this.updateTitleContainerTitle((item.getTitle) ? item.getTitle() : item.config.title);
112343 }
112344
112345 if (this.initialized) {
112346 this.fireEvent('add', this, item, index);
112347 }
112348 },
112349
112350 /**
112351 * @private
112352 * Updates the title of the titleContainer, if it exists
112353 */
112354 updateTitleContainerTitle: function(title) {
112355 if (this.$titleContainer) {
112356 //<debug>
112357 if (!this.$titleContainer.setTitle) {
112358 Ext.Logger.error('You have selected to display a title in a component that does not \
112359 support titles in NavigationView. Please remove the `title` configuration from your \
112360 NavigationView item, or change it to a component that has a `setTitle` method.');
112361 }
112362 //</debug>
112363
112364 this.$titleContainer.setTitle(title);
112365 }
112366 else {
112367 this.$currentTitle = title;
112368 }
112369 },
112370
112371 /**
112372 * Resets the view by removing all items between the first and last item.
112373 * @return {Ext.Component} The view that is now active
112374 */
112375 reset: function() {
112376 return this.pop(this.getInnerItems().length);
112377 }
112378 });
112379
112380 /**
112381 * Adds a Load More button at the bottom of the list. When the user presses this button,
112382 * the next page of data will be loaded into the store and appended to the List.
112383 *
112384 * By specifying `{@link #autoPaging}: true`, an 'infinite scroll' effect can be achieved,
112385 * i.e., the next page of content will load automatically when the user scrolls to the
112386 * bottom of the list.
112387 *
112388 * ## Example
112389 *
112390 * Ext.create('Ext.dataview.List', {
112391 *
112392 * store: Ext.create('TweetStore'),
112393 *
112394 * plugins: [
112395 * {
112396 * xclass: 'Ext.plugin.ListPaging',
112397 * autoPaging: true
112398 * }
112399 * ],
112400 *
112401 * itemTpl: [
112402 * '<img src="{profile_image_url}" />',
112403 * '<div class="tweet">{text}</div>'
112404 * ]
112405 * });
112406 */
112407 Ext.define('Ext.plugin.ListPaging', {
112408 extend: Ext.Component ,
112409 alias: 'plugin.listpaging',
112410
112411 config: {
112412 /**
112413 * @cfg {Boolean} autoPaging
112414 * True to automatically load the next page when you scroll to the bottom of the list.
112415 */
112416 autoPaging: false,
112417
112418 /**
112419 * @cfg {String} loadMoreText The text used as the label of the Load More button.
112420 */
112421 loadMoreText: 'Load More...',
112422
112423 /**
112424 * @cfg {String} noMoreRecordsText The text used as the label of the Load More button when the Store's
112425 * {@link Ext.data.Store#totalCount totalCount} indicates that all of the records available on the server are
112426 * already loaded
112427 */
112428 noMoreRecordsText: 'No More Records',
112429
112430 /**
112431 * @private
112432 * @cfg {String} loadTpl The template used to render the load more text
112433 */
112434 loadTpl: [
112435 '<div class="{cssPrefix}loading-spinner" style="font-size: 180%; margin: 10px auto;">',
112436 '<span class="{cssPrefix}loading-top"></span>',
112437 '<span class="{cssPrefix}loading-right"></span>',
112438 '<span class="{cssPrefix}loading-bottom"></span>',
112439 '<span class="{cssPrefix}loading-left"></span>',
112440 '</div>',
112441 '<div class="{cssPrefix}list-paging-msg">{message}</div>'
112442 ].join(''),
112443
112444 /**
112445 * @cfg {Object} loadMoreCmp
112446 * @private
112447 */
112448 loadMoreCmp: {
112449 xtype: 'component',
112450 baseCls: Ext.baseCSSPrefix + 'list-paging',
112451 scrollDock: 'bottom',
112452 hidden: true
112453 },
112454
112455 /**
112456 * @private
112457 * @cfg {Boolean} loadMoreCmpAdded Indicates whether or not the load more component has been added to the List
112458 * yet.
112459 */
112460 loadMoreCmpAdded: false,
112461
112462 /**
112463 * @private
112464 * @cfg {String} loadingCls The CSS class that is added to the {@link #loadMoreCmp} while the Store is loading
112465 */
112466 loadingCls: Ext.baseCSSPrefix + 'loading',
112467
112468 /**
112469 * @private
112470 * @cfg {Ext.List} list Local reference to the List this plugin is bound to
112471 */
112472 list: null,
112473
112474 /**
112475 * @private
112476 * @cfg {Ext.scroll.Scroller} scroller Local reference to the List's Scroller
112477 */
112478 scroller: null,
112479
112480 /**
112481 * @private
112482 * @cfg {Boolean} loading True if the plugin has initiated a Store load that has not yet completed
112483 */
112484 loading: false
112485 },
112486
112487 /**
112488 * @private
112489 * Sets up all of the references the plugin needs
112490 */
112491 init: function(list) {
112492 var scroller = list.getScrollable().getScroller(),
112493 store = list.getStore();
112494
112495 this.setList(list);
112496 this.setScroller(scroller);
112497 this.bindStore(list.getStore());
112498
112499 this.addLoadMoreCmp();
112500
112501 // The List's Store could change at any time so make sure we are informed when that happens
112502 list.updateStore = Ext.Function.createInterceptor(list.updateStore, this.bindStore, this);
112503
112504 if (this.getAutoPaging()) {
112505 scroller.on({
112506 scrollend: this.onScrollEnd,
112507 scope: this
112508 });
112509 }
112510 },
112511
112512 /**
112513 * @private
112514 */
112515 bindStore: function(newStore, oldStore) {
112516 if (oldStore) {
112517 oldStore.un({
112518 beforeload: this.onStoreBeforeLoad,
112519 load: this.onStoreLoad,
112520 filter: this.onFilter,
112521 scope: this
112522 });
112523 }
112524
112525 if (newStore) {
112526 newStore.on({
112527 beforeload: this.onStoreBeforeLoad,
112528 load: this.onStoreLoad,
112529 filter: this.onFilter,
112530 scope: this
112531 });
112532 }
112533 },
112534
112535 /**
112536 * @private
112537 * Removes the List/DataView's loading mask because we show our own in the plugin. The logic here disables the
112538 * loading mask immediately if the store is autoloading. If it's not autoloading, allow the mask to show the first
112539 * time the Store loads, then disable it and use the plugin's loading spinner.
112540 * @param {Ext.data.Store} store The store that is bound to the DataView
112541 */
112542 disableDataViewMask: function() {
112543 var list = this.getList();
112544 this._listMask = list.getLoadingText();
112545
112546 list.setLoadingText(null);
112547 },
112548
112549 enableDataViewMask: function() {
112550 if(this._listMask) {
112551 var list = this.getList();
112552 list.setLoadingText(this._listMask);
112553 delete this._listMask;
112554 }
112555 },
112556
112557 /**
112558 * @private
112559 */
112560 applyLoadTpl: function(config) {
112561 return (Ext.isObject(config) && config.isTemplate) ? config : new Ext.XTemplate(config);
112562 },
112563
112564 /**
112565 * @private
112566 */
112567 applyLoadMoreCmp: function(config) {
112568 config = Ext.merge(config, {
112569 html: this.getLoadTpl().apply({
112570 cssPrefix: Ext.baseCSSPrefix,
112571 message: this.getLoadMoreText()
112572 }),
112573 scrollDock: 'bottom',
112574 listeners: {
112575 tap: {
112576 fn: this.loadNextPage,
112577 scope: this,
112578 element: 'element'
112579 }
112580 }
112581 });
112582
112583 return Ext.factory(config, Ext.Component, this.getLoadMoreCmp());
112584 },
112585
112586 /**
112587 * @private
112588 * If we're using autoPaging and detect that the user has scrolled to the bottom, kick off loading of the next page
112589 */
112590 onScrollEnd: function(scroller, x, y) {
112591 var list = this.getList();
112592
112593 if (!this.getLoading() && y >= scroller.maxPosition.y) {
112594 this.currentScrollToTopOnRefresh = list.getScrollToTopOnRefresh();
112595 list.setScrollToTopOnRefresh(false);
112596
112597 this.loadNextPage();
112598 }
112599 },
112600
112601 /**
112602 * @private
112603 * Makes sure we add/remove the loading CSS class while the Store is loading
112604 */
112605 updateLoading: function(isLoading) {
112606 var loadMoreCmp = this.getLoadMoreCmp(),
112607 loadMoreCls = this.getLoadingCls();
112608
112609 if (isLoading) {
112610 loadMoreCmp.addCls(loadMoreCls);
112611 } else {
112612 loadMoreCmp.removeCls(loadMoreCls);
112613 }
112614 },
112615
112616 /**
112617 * @private
112618 * If the Store is just about to load but it's currently empty, we hide the load more button because this is
112619 * usually an outcome of setting a new Store on the List so we don't want the load more button to flash while
112620 * the new Store loads
112621 */
112622 onStoreBeforeLoad: function(store) {
112623 if (store.getCount() === 0) {
112624 this.getLoadMoreCmp().hide();
112625 }
112626 },
112627
112628 /**
112629 * @private
112630 */
112631 onStoreLoad: function(store) {
112632 var loadCmp = this.getLoadMoreCmp(),
112633 template = this.getLoadTpl(),
112634 message = this.storeFullyLoaded() ? this.getNoMoreRecordsText() : this.getLoadMoreText();
112635
112636 if (store.getCount()) {
112637 loadCmp.show();
112638 }
112639 this.setLoading(false);
112640
112641 //if we've reached the end of the data set, switch to the noMoreRecordsText
112642 loadCmp.setHtml(template.apply({
112643 cssPrefix: Ext.baseCSSPrefix,
112644 message: message
112645 }));
112646
112647 if (this.currentScrollToTopOnRefresh !== undefined) {
112648 this.getList().setScrollToTopOnRefresh(this.currentScrollToTopOnRefresh);
112649 delete this.currentScrollToTopOnRefresh;
112650 }
112651
112652 this.enableDataViewMask();
112653 },
112654
112655 onFilter: function(store) {
112656 if (store.getCount() === 0) {
112657 this.getLoadMoreCmp().hide();
112658 }else {
112659 this.getLoadMoreCmp().show();
112660 }
112661 },
112662
112663 /**
112664 * @private
112665 * Because the attached List's inner list element is rendered after our init function is called,
112666 * we need to dynamically add the loadMoreCmp later. This does this once and caches the result.
112667 */
112668 addLoadMoreCmp: function() {
112669 var list = this.getList(),
112670 cmp = this.getLoadMoreCmp();
112671
112672 if (!this.getLoadMoreCmpAdded()) {
112673 list.add(cmp);
112674
112675 /**
112676 * @event loadmorecmpadded Fired when the Load More component is added to the list. Fires on the List.
112677 * @param {Ext.plugin.ListPaging} this The list paging plugin
112678 * @param {Ext.List} list The list
112679 */
112680 list.fireEvent('loadmorecmpadded', this, list);
112681 this.setLoadMoreCmpAdded(true);
112682 }
112683
112684 return cmp;
112685 },
112686
112687 /**
112688 * @private
112689 * Returns true if the Store is detected as being fully loaded, or the server did not return a total count, which
112690 * means we're in 'infinite' mode
112691 * @return {Boolean}
112692 */
112693 storeFullyLoaded: function() {
112694 var store = this.getList().getStore(),
112695 total = store.getTotalCount();
112696
112697 return total !== null ? store.getTotalCount() <= (store.currentPage * store.getPageSize()) : false;
112698 },
112699
112700 /**
112701 * @private
112702 */
112703 loadNextPage: function() {
112704 var me = this;
112705 if (!me.storeFullyLoaded()) {
112706 me.disableDataViewMask();
112707 me.setLoading(true);
112708 me.getList().getStore().nextPage({ addRecords: true });
112709 }
112710 }
112711 });
112712
112713 /**
112714 * This plugin adds pull to refresh functionality to the List.
112715 *
112716 * ## Example
112717 *
112718 * @example
112719 * var store = Ext.create('Ext.data.Store', {
112720 * fields: ['name', 'img', 'text'],
112721 * data: [
112722 * {
112723 * name: 'rdougan',
112724 * img: 'http://a0.twimg.com/profile_images/1261180556/171265_10150129602722922_727937921_7778997_8387690_o_reasonably_small.jpg',
112725 * text: 'JavaScript development'
112726 * }
112727 * ]
112728 * });
112729 *
112730 * Ext.create('Ext.dataview.List', {
112731 * fullscreen: true,
112732 *
112733 * store: store,
112734 *
112735 * plugins: [
112736 * {
112737 * xclass: 'Ext.plugin.PullRefresh',
112738 * pullText: 'Pull down for more new Tweets!'
112739 * }
112740 * ],
112741 *
112742 * itemTpl: [
112743 * '<img src="{img}" alt="{name} photo" />',
112744 * '<div class="tweet"><b>{name}:</b> {text}</div>'
112745 * ]
112746 * });
112747 */
112748 Ext.define('Ext.plugin.PullRefresh', {
112749 extend: Ext.Component ,
112750 alias: 'plugin.pullrefresh',
112751
112752
112753 config: {
112754 width: '100%',
112755 /**
112756 * @cfg {Ext.dataview.List} list
112757 * The list to which this PullRefresh plugin is connected.
112758 * This will usually by set automatically when configuring the list with this plugin.
112759 * @accessor
112760 */
112761 list: null,
112762
112763 /**
112764 * @cfg {String} pullText The text that will be shown while you are pulling down.
112765 * @accessor
112766 */
112767 pullText: 'Pull down to refresh...',
112768
112769 /**
112770 * @cfg {String} releaseText The text that will be shown after you have pulled down enough to show the release message.
112771 * @accessor
112772 */
112773 releaseText: 'Release to refresh...',
112774
112775 /**
112776 * @cfg {String} loadingText The text that will be shown while the list is refreshing.
112777 * @accessor
112778 */
112779 loadingText: 'Loading...',
112780
112781 /**
112782 * @cfg {String} loadedText The text that will be when data has been loaded.
112783 * @accessor
112784 */
112785 loadedText: 'Loaded.',
112786
112787 /**
112788 * @cfg {String} lastUpdatedText The text to be shown in front of the last updated time.
112789 * @accessor
112790 */
112791 lastUpdatedText: 'Last Updated:&nbsp;',
112792
112793 /**
112794 * @cfg {Boolean} scrollerAutoRefresh Determines whether the attached scroller should automatically track size changes of its container.
112795 * Enabling this will have performance impacts but will be necessary if your list size changes dynamically. For example if your list contains images
112796 * that will be loading and have unspecified heights.
112797 */
112798 scrollerAutoRefresh: false,
112799
112800 /**
112801 * @cfg {Boolean} autoSnapBack Determines whether the pulldown should automatically snap back after data has been loaded.
112802 * If false call {@link #snapBack}() to manually snap the pulldown back.
112803 */
112804 autoSnapBack: true,
112805
112806 /**
112807 * @cfg {Number} snappingAnimationDuration The duration for snapping back animation after the data has been refreshed
112808 * @accessor
112809 */
112810 snappingAnimationDuration: 300,
112811 /**
112812 * @cfg {String} lastUpdatedDateFormat The format to be used on the last updated date.
112813 */
112814 lastUpdatedDateFormat: 'm/d/Y h:iA',
112815
112816 /**
112817 * @cfg {Number} overpullSnapBackDuration The duration for snapping back when pulldown has been lowered further then its height.
112818 */
112819 overpullSnapBackDuration: 300,
112820
112821 /**
112822 * @cfg {Ext.XTemplate/String/Array} pullTpl The template being used for the pull to refresh markup.
112823 * Will be passed a config object with properties state, message and updated
112824 *
112825 * @accessor
112826 */
112827 pullTpl: [
112828 '<div class="x-list-pullrefresh-arrow"></div>',
112829 '<div class="x-loading-spinner">',
112830 '<span class="x-loading-top"></span>',
112831 '<span class="x-loading-right"></span>',
112832 '<span class="x-loading-bottom"></span>',
112833 '<span class="x-loading-left"></span>',
112834 '</div>',
112835 '<div class="x-list-pullrefresh-wrap">',
112836 '<h3 class="x-list-pullrefresh-message">{message}</h3>',
112837 '<div class="x-list-pullrefresh-updated">{updated}</div>',
112838 '</div>'
112839 ].join(''),
112840
112841 translatable: true
112842 },
112843
112844 // @private
112845 $state: "pull",
112846 // @private
112847 getState: function() {
112848 return this.$state
112849 },
112850 // @private
112851 setState: function(value) {
112852 this.$state = value;
112853 this.updateView();
112854 },
112855 // @private
112856 $isSnappingBack: false,
112857 // @private
112858 getIsSnappingBack: function() {
112859 return this.$isSnappingBack;
112860 },
112861 // @private
112862 setIsSnappingBack: function(value) {
112863 this.$isSnappingBack = value;
112864 },
112865
112866 // @private
112867 init: function(list) {
112868 var me = this;
112869
112870 me.setList(list);
112871 me.initScrollable();
112872 },
112873
112874 getElementConfig: function() {
112875 return {
112876 reference: 'element',
112877 classList: ['x-unsized'],
112878 children: [
112879 {
112880 reference: 'innerElement',
112881 className: Ext.baseCSSPrefix + 'list-pullrefresh'
112882 }
112883 ]
112884 };
112885 },
112886
112887 // @private
112888 initScrollable: function() {
112889 var me = this,
112890 list = me.getList(),
112891 scrollable = list.getScrollable(),
112892 scroller;
112893
112894 if (!scrollable) {
112895 return;
112896 }
112897
112898 scroller = scrollable.getScroller();
112899 scroller.setAutoRefresh(this.getScrollerAutoRefresh());
112900
112901 me.lastUpdated = new Date();
112902
112903 list.insert(0, me);
112904
112905 scroller.on({
112906 scroll: me.onScrollChange,
112907 scope: me
112908 });
112909
112910 this.updateView();
112911 },
112912
112913 // @private
112914 applyPullTpl: function(config) {
112915 if (config instanceof Ext.XTemplate) {
112916 return config
112917 } else {
112918 return new Ext.XTemplate(config);
112919 }
112920 },
112921
112922 // @private
112923 updateList: function(newList, oldList) {
112924 var me = this;
112925
112926 if (newList && newList != oldList) {
112927 newList.on({
112928 order: 'after',
112929 scrollablechange: me.initScrollable,
112930 scope: me
112931 });
112932 } else if (oldList) {
112933 oldList.un({
112934 order: 'after',
112935 scrollablechange: me.initScrollable,
112936 scope: me
112937 });
112938 }
112939 },
112940
112941 // @private
112942 getPullHeight: function() {
112943 return this.innerElement.getHeight();
112944 },
112945
112946 /**
112947 * @private
112948 * Attempts to load the newest posts via the attached List's Store's Proxy
112949 */
112950 fetchLatest: function() {
112951 var store = this.getList().getStore(),
112952 proxy = store.getProxy(),
112953 operation;
112954
112955 operation = Ext.create('Ext.data.Operation', {
112956 page: 1,
112957 start: 0,
112958 model: store.getModel(),
112959 limit: store.getPageSize(),
112960 action: 'read',
112961 sorters: store.getSorters(),
112962 filters: store.getRemoteFilter() ? store.getFilters() : []
112963 });
112964
112965 proxy.read(operation, this.onLatestFetched, this);
112966 },
112967
112968 /**
112969 * @private
112970 * Called after fetchLatest has finished grabbing data. Matches any returned records against what is already in the
112971 * Store. If there is an overlap, updates the existing records with the new data and inserts the new items at the
112972 * front of the Store. If there is no overlap, insert the new records anyway and record that there's a break in the
112973 * timeline between the new and the old records.
112974 */
112975 onLatestFetched: function(operation) {
112976 var store = this.getList().getStore(),
112977 oldRecords = store.getData(),
112978 newRecords = operation.getRecords(),
112979 length = newRecords.length,
112980 toInsert = [],
112981 newRecord, oldRecord, i;
112982
112983 for (i = 0; i < length; i++) {
112984 newRecord = newRecords[i];
112985 oldRecord = oldRecords.getByKey(newRecord.getId());
112986
112987 if (oldRecord) {
112988 oldRecord.set(newRecord.getData());
112989 } else {
112990 toInsert.push(newRecord);
112991 }
112992
112993 oldRecord = undefined;
112994 }
112995
112996 store.insert(0, toInsert);
112997 this.setState("loaded");
112998 this.fireEvent('latestfetched', this, toInsert);
112999 if (this.getAutoSnapBack()) {
113000 this.snapBack();
113001 }
113002 },
113003
113004 /**
113005 * Snaps the List back to the top after a pullrefresh is complete
113006 * @param {Boolean=} force Force the snapback to occur regardless of state {optional}
113007 */
113008 snapBack: function(force) {
113009 if(this.getState() !== "loaded" && force !== true) return;
113010
113011 var list = this.getList(),
113012 scroller = list.getScrollable().getScroller();
113013
113014 scroller.refresh();
113015 scroller.minPosition.y = 0;
113016
113017 scroller.on({
113018 scrollend: this.onSnapBackEnd,
113019 single: true,
113020 scope: this
113021 });
113022
113023 this.setIsSnappingBack(true);
113024 scroller.scrollTo(null, 0, {duration: this.getSnappingAnimationDuration()});
113025 },
113026
113027 /**
113028 * @private
113029 * Called when PullRefresh has been snapped back to the top
113030 */
113031 onSnapBackEnd: function() {
113032 this.setState("pull");
113033 this.setIsSnappingBack(false);
113034 },
113035
113036 /**
113037 * @private
113038 * Called when the Scroller updates from the list
113039 * @param scroller
113040 * @param x
113041 * @param y
113042 */
113043 onScrollChange: function(scroller, x, y) {
113044 if (y <= 0) {
113045 var pullHeight = this.getPullHeight(),
113046 isSnappingBack = this.getIsSnappingBack();
113047
113048 if(this.getState() === "loaded" && !isSnappingBack) {
113049 this.snapBack();
113050 }
113051
113052 if (this.getState() !== "loading" && this.getState() !=="loaded") {
113053 if (-y >= pullHeight + 10) {
113054 this.setState("release");
113055 scroller.getContainer().onBefore({
113056 dragend: 'onScrollerDragEnd',
113057 single: true,
113058 scope: this
113059 });
113060 } else if ((this.getState() === "release") && (-y < pullHeight + 10)) {
113061 this.setState("pull");
113062 scroller.getContainer().unBefore({
113063 dragend: 'onScrollerDragEnd',
113064 single: true,
113065 scope: this
113066 });
113067 }
113068 }
113069 this.getTranslatable().translate(0, -y);
113070 }
113071 },
113072
113073 /**
113074 * @private
113075 * Called when the user is done dragging, this listener is only added when the user has pulled far enough for a refresh
113076 */
113077 onScrollerDragEnd: function() {
113078 if (this.getState() !== "loading") {
113079 var list = this.getList(),
113080 scroller = list.getScrollable().getScroller(),
113081 translateable = scroller.getTranslatable();
113082
113083 this.setState("loading");
113084 translateable.setEasingY({duration: this.getOverpullSnapBackDuration()});
113085 scroller.minPosition.y = -this.getPullHeight();
113086 scroller.on({
113087 scrollend: 'fetchLatest',
113088 single: true,
113089 scope: this
113090 });
113091 }
113092 },
113093
113094 /**
113095 * @private
113096 * Updates the content based on the PullRefresh Template
113097 */
113098 updateView: function() {
113099 var state = this.getState(),
113100 lastUpdatedText = this.getLastUpdatedText() + Ext.util.Format.date(this.lastUpdated, this.getLastUpdatedDateFormat()),
113101 templateConfig = {state: state, updated: lastUpdatedText},
113102 stateFn = state.charAt(0).toUpperCase() + state.slice(1).toLowerCase(),
113103 fn = "get" + stateFn + "Text";
113104
113105 if (this[fn] && Ext.isFunction(this[fn])) {
113106 templateConfig.message = this[fn].call(this);
113107 }
113108
113109 this.innerElement.removeCls(["loaded", "loading", "release", "pull"], Ext.baseCSSPrefix + "list-pullrefresh");
113110 this.innerElement.addCls(this.getState(), Ext.baseCSSPrefix + "list-pullrefresh");
113111 this.getPullTpl().overwrite(this.innerElement, templateConfig);
113112 }
113113 }, function() {
113114 });
113115
113116 /**
113117 * @class Ext.plugin.SortableList
113118 * @extends Ext.Component
113119 * The SortableList plugin gives your list items the ability to be reordered by tapping and
113120 * dragging elements within the item.
113121 *
113122 * The list-sortablehandle is not added to your tpl by default, so it's important that you
113123 * manually include it. It's also important to recognize that list-items are not draggable
113124 * themselves. You must add an element to the itemTpl for it to be dragged.
113125 *
113126 * Ext.Viewport.add({
113127 * xtype: 'list',
113128 * infinite: true,
113129 * plugins: 'sortablelist',
113130 * itemTpl: '<span class="myStyle ' + Ext.baseCSSPrefix + 'list-sortablehandle"></span>{text}',
113131 * data: [{
113132 * text: 'Item 1'
113133 * }, {
113134 * text: 'Item 2'
113135 * }, {
113136 * text: 'Item 3'
113137 * }]
113138 * });
113139 *
113140 * The CSS for MyStyle can be anything that creates an element to tap and drag. For this
113141 * example we made a simple rectangle like so:
113142 *
113143 * .myStyle{
113144 * width:30px;
113145 * height:20px;
113146 * background:gray;
113147 * float:left;
113148 * }
113149 *
113150 * Note: You must have infinite set to 'true' when using the SortableList plugin.
113151 *
113152 */
113153 Ext.define('Ext.plugin.SortableList', {
113154 extend: Ext.Component ,
113155
113156 alias: 'plugin.sortablelist',
113157
113158 mixins: [ Ext.mixin.Bindable ],
113159
113160 config: {
113161 list: null,
113162 handleSelector: '.' + Ext.baseCSSPrefix + 'list-sortablehandle'
113163 },
113164
113165 init: function(list) {
113166 this.setList(list);
113167 },
113168
113169 updateList: function(list) {
113170 if (list) {
113171 if (list.initialized) {
113172 this.attachListeners();
113173 }
113174 else {
113175 list.on({
113176 initialize: 'attachListeners',
113177 scope: this,
113178 single: true
113179 });
113180 }
113181 }
113182 },
113183
113184 attachListeners: function() {
113185 var list = this.getList(),
113186 scrollerElement = list.getScrollable().getScroller().getContainer();
113187
113188 this.scrollerElement = scrollerElement;
113189
113190 scrollerElement.onBefore({
113191 dragstart: 'onScrollerDragStart',
113192 scope: this
113193 });
113194 },
113195
113196 onScrollerDragStart: function(e, target) {
113197 if (Ext.DomQuery.is(target, this.getHandleSelector())) {
113198 if (!this.animating) {
113199 this.onDragStart(e, target);
113200 }
113201 return false;
113202 }
113203 },
113204
113205 onDragStart: function(e) {
113206 var row = Ext.getCmp(e.getTarget('.' + Ext.baseCSSPrefix + 'list-item').id),
113207 list = this.getList(),
113208 store = list.getStore();
113209
113210 this.scrollerElement.on({
113211 drag: 'onDrag',
113212 dragend: 'onDragEnd',
113213 scope: this
113214 });
113215
113216 this.positionMap = list.getItemMap();
113217 this.listStore = store;
113218 this.previousIndexDistance = 0;
113219
113220 this.dragRow = row;
113221 this.dragRecord = row.getRecord();
113222
113223 this.dragRowIndex = this.currentDragRowIndex = row.$dataIndex;
113224 this.dragRowHeight = this.positionMap.getItemHeight(this.dragRowIndex);
113225
113226 if (list.getInfinite()) {
113227 this.startTranslate = this.positionMap.map[this.dragRowIndex];
113228 } else {
113229 row.translate(0, 0);
113230 this.startTranslate = 0;
113231 }
113232
113233 row.addCls(Ext.baseCSSPrefix + 'list-item-dragging');
113234 },
113235
113236 onDrag: function(e) {
113237 var list = this.getList(),
113238 listItems = list.listItems,
113239 collection = list.getStore().data,
113240 dragRow = this.dragRow,
113241 dragRecordKey = collection.getKey(dragRow.getRecord()),
113242 listItemInfo = list.getListItemInfo(),
113243 positionMap = this.positionMap,
113244 distance = 0,
113245 i, item, ln, targetItem, targetIndex, itemIndex,
113246 swapIndex, swapPosition, record, swapKey, draggingUp;
113247
113248 this.dragRowPosition = this.startTranslate + e.deltaY;
113249 dragRow.translate(0, this.dragRowPosition);
113250
113251 targetIndex = positionMap.findIndex(this.dragRowPosition + (this.dragRowHeight / 2));
113252 targetItem = list.getItemAt(targetIndex);
113253
113254 if (targetItem) {
113255 distance = targetIndex - this.currentDragRowIndex;
113256
113257 if (distance !== 0) {
113258 draggingUp = (distance < 0);
113259
113260 for (i = 0, ln = Math.abs(distance); i < ln; i++) {
113261 if (draggingUp) {
113262 swapIndex = this.currentDragRowIndex - i;
113263 item = list.getItemAt(swapIndex - 1);
113264 } else {
113265 swapIndex = this.currentDragRowIndex + i;
113266 item = list.getItemAt(swapIndex + 1);
113267 }
113268
113269 swapPosition = positionMap.map[swapIndex];
113270
113271 item.translate(0, swapPosition);
113272
113273 record = item.getRecord();
113274 swapKey = collection.getKey(record);
113275
113276 Ext.Array.remove(collection.items, record);
113277 Ext.Array.remove(collection.all, record);
113278 collection.items.splice(swapIndex, 0, record);
113279 collection.all.splice(swapIndex, 0, record);
113280 collection.indices[dragRecordKey] = collection.indices[swapKey];
113281 collection.indices[swapKey] = swapIndex;
113282
113283 list.updateListItem(item, swapIndex, listItemInfo);
113284 item.$position = swapPosition;
113285 }
113286
113287 itemIndex = listItems.indexOf(dragRow);
113288 Ext.Array.remove(listItems, dragRow);
113289 listItems.splice(itemIndex + distance, 0, dragRow);
113290
113291 dragRow.$dataIndex = targetIndex;
113292 dragRow.$position = positionMap.map[targetIndex];
113293
113294 this.currentDragRowIndex = targetIndex;
113295 }
113296 }
113297 },
113298
113299 onDragEnd: function() {
113300 var me = this,
113301 row = this.dragRow,
113302 list = this.getList(),
113303 listItemInfo = list.getListItemInfo(),
113304 position = row.$position;
113305
113306 this.scrollerElement.un({
113307 drag: 'onDrag',
113308 dragend: 'onDragEnd',
113309 scope: this
113310 });
113311
113312 this.animating = true;
113313
113314 row.getTranslatable().on('animationend', function() {
113315 row.removeCls(Ext.baseCSSPrefix + 'list-item-dragging');
113316
113317 list.updateListItem(row, row.$dataIndex, listItemInfo);
113318 row.$position = position;
113319
113320 list.fireEvent('dragsort', list, row, this.currentDragRowIndex, this.dragRowIndex);
113321 this.animating = false;
113322 }, me, {single: true});
113323
113324 row.translate(0, position, {duration: 100});
113325 }
113326 });
113327
113328 /**
113329 * Used in the {@link Ext.tab.Bar} component. This shouldn't be used directly, instead use
113330 * {@link Ext.tab.Bar} or {@link Ext.tab.Panel}.
113331 * @private
113332 */
113333 Ext.define('Ext.tab.Tab', {
113334 extend: Ext.Button ,
113335 xtype: 'tab',
113336 alternateClassName: 'Ext.Tab',
113337
113338 // @private
113339 isTab: true,
113340
113341 config: {
113342 /**
113343 * @cfg baseCls
113344 * @inheritdoc
113345 */
113346 baseCls: Ext.baseCSSPrefix + 'tab',
113347
113348 /**
113349 * @cfg {String} pressedCls
113350 * The CSS class to be applied to a Tab when it is pressed.
113351 * Providing your own CSS for this class enables you to customize the pressed state.
113352 * @accessor
113353 */
113354 pressedCls: Ext.baseCSSPrefix + 'tab-pressed',
113355
113356 /**
113357 * @cfg {String} activeCls
113358 * The CSS class to be applied to a Tab when it is active.
113359 * Providing your own CSS for this class enables you to customize the active state.
113360 * @accessor
113361 */
113362 activeCls: Ext.baseCSSPrefix + 'tab-active',
113363
113364 /**
113365 * @cfg {Boolean} active
113366 * Set this to `true` to have the tab be active by default.
113367 * @accessor
113368 */
113369 active: false,
113370
113371 /**
113372 * @cfg {String} title
113373 * The title of the card that this tab is bound to.
113374 * @accessor
113375 */
113376 title: '&nbsp;'
113377 },
113378
113379 updateIconCls : function(newCls, oldCls) {
113380 this.callParent([newCls, oldCls]);
113381
113382 if (oldCls) {
113383 this.removeCls('x-tab-icon');
113384 }
113385
113386 if (newCls) {
113387 this.addCls('x-tab-icon');
113388 }
113389 },
113390
113391 /**
113392 * @event activate
113393 * Fires when a tab is activated
113394 * @param {Ext.tab.Tab} this
113395 */
113396
113397 /**
113398 * @event deactivate
113399 * Fires when a tab is deactivated
113400 * @param {Ext.tab.Tab} this
113401 */
113402
113403 updateTitle: function(title) {
113404 this.setText(title);
113405 },
113406
113407 updateActive: function(active, oldActive) {
113408 var activeCls = this.getActiveCls();
113409 if (active && !oldActive) {
113410 this.element.addCls(activeCls);
113411 this.fireEvent('activate', this);
113412 } else if (oldActive) {
113413 this.element.removeCls(activeCls);
113414 this.fireEvent('deactivate', this);
113415 }
113416 }
113417 }, function() {
113418 this.override({
113419 activate: function() {
113420 this.setActive(true);
113421 },
113422
113423 deactivate: function() {
113424 this.setActive(false);
113425 }
113426 });
113427 });
113428
113429 /**
113430 * Ext.tab.Bar is used internally by {@link Ext.tab.Panel} to create the bar of tabs that appears at the top of the tab
113431 * panel. It's unusual to use it directly, instead see the {@link Ext.tab.Panel tab panel docs} for usage instructions.
113432 *
113433 * Used in the {@link Ext.tab.Panel} component to display {@link Ext.tab.Tab} components.
113434 *
113435 * @private
113436 */
113437 Ext.define('Ext.tab.Bar', {
113438 extend: Ext.Toolbar ,
113439 alternateClassName: 'Ext.TabBar',
113440 xtype : 'tabbar',
113441
113442
113443
113444 config: {
113445 /**
113446 * @cfg baseCls
113447 * @inheritdoc
113448 */
113449 baseCls: Ext.baseCSSPrefix + 'tabbar',
113450
113451 // @private
113452 defaultType: 'tab',
113453
113454 // @private
113455 layout: {
113456 type: 'hbox',
113457 align: 'middle'
113458 }
113459 },
113460
113461 eventedConfig: {
113462 /**
113463 * @cfg {Number/String/Ext.Component} activeTab
113464 * The initially activated tab. Can be specified as numeric index,
113465 * component ID or as the component instance itself.
113466 * @accessor
113467 * @evented
113468 */
113469 activeTab: null
113470 },
113471
113472 /**
113473 * @event tabchange
113474 * Fired when active tab changes.
113475 * @param {Ext.tab.Bar} this
113476 * @param {Ext.tab.Tab} newTab The new Tab
113477 * @param {Ext.tab.Tab} oldTab The old Tab
113478 */
113479
113480 platformConfig: [{
113481 theme: ['Blackberry', 'Blackberry103', 'CupertinoClassic', 'MountainView'],
113482 defaults: {
113483 flex: 1
113484 }
113485 }],
113486
113487 initialize: function() {
113488 var me = this;
113489 me.callParent();
113490
113491 me.on({
113492 tap: 'onTabTap',
113493
113494 delegate: '> tab',
113495 scope : me
113496 });
113497 },
113498
113499 // @private
113500 onTabTap: function(tab) {
113501 this.setActiveTab(tab);
113502 },
113503
113504 /**
113505 * @private
113506 */
113507 applyActiveTab: function(newActiveTab, oldActiveTab) {
113508 if (!newActiveTab && newActiveTab !== 0) {
113509 return;
113510 }
113511
113512 var newTabInstance = this.parseActiveTab(newActiveTab);
113513
113514 if (!newTabInstance) {
113515 // <debug warn>
113516 if (oldActiveTab) {
113517 Ext.Logger.warn('Trying to set a non-existent activeTab');
113518 }
113519 // </debug>
113520 return;
113521 }
113522 return newTabInstance;
113523 },
113524
113525 /**
113526 * @private
113527 * Default pack to center when docked to the bottom, otherwise default pack to left
113528 */
113529 doSetDocked: function(newDocked) {
113530 var layout = this.getLayout(),
113531 initialConfig = this.getInitialConfig(),
113532 pack;
113533
113534 if (!initialConfig.layout || !initialConfig.layout.pack) {
113535 pack = (newDocked == 'bottom') ? 'center' : 'left';
113536 //layout isn't guaranteed to be instantiated so must test
113537 if (layout.isLayout) {
113538 layout.setPack(pack);
113539 } else {
113540 layout.pack = (layout && layout.pack) ? layout.pack : pack;
113541 }
113542 }
113543
113544 this.callParent(arguments);
113545 },
113546
113547 /**
113548 * @private
113549 * Sets the active tab
113550 */
113551 doSetActiveTab: function(newTab, oldTab) {
113552 if (newTab) {
113553 newTab.setActive(true);
113554 }
113555
113556 //Check if the parent is present, if not it is destroyed
113557 if (oldTab && oldTab.parent) {
113558 oldTab.setActive(false);
113559 }
113560 },
113561
113562 /**
113563 * @private
113564 * Parses the active tab, which can be a number or string
113565 */
113566 parseActiveTab: function(tab) {
113567 //we need to call getItems to initialize the items, otherwise they will not exist yet.
113568 if (typeof tab == 'number') {
113569 return this.getItems().items[tab];
113570 }
113571 else if (typeof tab == 'string') {
113572 tab = Ext.getCmp(tab);
113573 }
113574 return tab;
113575 }
113576 });
113577
113578 /**
113579 * Tab Panels are a great way to allow the user to switch between several pages that are all full screen. Each
113580 * Component in the Tab Panel gets its own Tab, which shows the Component when tapped on. Tabs can be positioned at
113581 * the top or the bottom of the Tab Panel, and can optionally accept title and icon configurations.
113582 *
113583 * Here's how we can set up a simple Tab Panel with tabs at the bottom. Use the controls at the top left of the example
113584 * to toggle between code mode and live preview mode (you can also edit the code and see your changes in the live
113585 * preview):
113586 *
113587 * @example miniphone preview
113588 * Ext.create('Ext.TabPanel', {
113589 * fullscreen: true,
113590 * tabBarPosition: 'bottom',
113591 *
113592 * defaults: {
113593 * styleHtmlContent: true
113594 * },
113595 *
113596 * items: [
113597 * {
113598 * title: 'Home',
113599 * iconCls: 'home',
113600 * html: 'Home Screen'
113601 * },
113602 * {
113603 * title: 'Contact',
113604 * iconCls: 'user',
113605 * html: 'Contact Screen'
113606 * }
113607 * ]
113608 * });
113609 * One tab was created for each of the {@link Ext.Panel panels} defined in the items array. Each tab automatically uses
113610 * the title and icon defined on the item configuration, and switches to that item when tapped on. We can also position
113611 * the tab bar at the top, which makes our Tab Panel look like this:
113612 *
113613 * @example miniphone preview
113614 * Ext.create('Ext.TabPanel', {
113615 * fullscreen: true,
113616 *
113617 * defaults: {
113618 * styleHtmlContent: true
113619 * },
113620 *
113621 * items: [
113622 * {
113623 * title: 'Home',
113624 * html: 'Home Screen'
113625 * },
113626 * {
113627 * title: 'Contact',
113628 * html: 'Contact Screen'
113629 * }
113630 * ]
113631 * });
113632 *
113633 * For more information, see our [Tab Panel Guide](../../../components/tabpanel.html).
113634 */
113635 Ext.define('Ext.tab.Panel', {
113636 extend: Ext.Container ,
113637 xtype : 'tabpanel',
113638 alternateClassName: 'Ext.TabPanel',
113639
113640
113641
113642 config: {
113643 /**
113644 * @cfg {String} ui
113645 * Sets the UI of this component.
113646 * Available values are: `light` and `dark`.
113647 * @accessor
113648 */
113649 ui: 'dark',
113650
113651 /**
113652 * @cfg {Object} tabBar
113653 * An Ext.tab.Bar configuration.
113654 * @accessor
113655 */
113656 tabBar: true,
113657
113658 /**
113659 * @cfg {String} tabBarPosition
113660 * The docked position for the {@link #tabBar} instance.
113661 * Possible values are 'top' and 'bottom'.
113662 * @accessor
113663 */
113664 tabBarPosition: 'top',
113665
113666 /**
113667 * @cfg layout
113668 * @inheritdoc
113669 */
113670 layout: {
113671 type: 'card',
113672 animation: {
113673 type: 'slide',
113674 direction: 'left'
113675 }
113676 },
113677
113678 /**
113679 * @cfg cls
113680 * @inheritdoc
113681 */
113682 cls: Ext.baseCSSPrefix + 'tabpanel'
113683
113684 /**
113685 * @cfg {Boolean/String/Object} scrollable
113686 * @accessor
113687 * @hide
113688 */
113689
113690 /**
113691 * @cfg {Boolean/String/Object} scroll
113692 * @hide
113693 */
113694 },
113695
113696 initialize: function() {
113697 this.callParent();
113698
113699 this.on({
113700 order: 'before',
113701 activetabchange: 'doTabChange',
113702 delegate: '> tabbar',
113703 scope : this
113704 });
113705
113706 this.on({
113707 disabledchange: 'onItemDisabledChange',
113708 delegate: '> component',
113709 scope : this
113710 });
113711 },
113712
113713 platformConfig: [{
113714 theme: ['Blackberry', 'Blackberry103'],
113715 tabBarPosition: 'bottom'
113716 }],
113717
113718 /**
113719 * Tab panels should not be scrollable. Instead, you should add scrollable to any item that
113720 * you want to scroll.
113721 * @private
113722 */
113723 applyScrollable: function() {
113724 return false;
113725 },
113726
113727 /**
113728 * Updates the Ui for this component and the {@link #tabBar}.
113729 */
113730 updateUi: function(newUi, oldUi) {
113731 this.callParent(arguments);
113732
113733 if (this.initialized) {
113734 this.getTabBar().setUi(newUi);
113735 }
113736 },
113737
113738 /**
113739 * @private
113740 */
113741 doSetActiveItem: function(newActiveItem, oldActiveItem) {
113742 if (newActiveItem) {
113743 var items = this.getInnerItems(),
113744 oldIndex = items.indexOf(oldActiveItem),
113745 newIndex = items.indexOf(newActiveItem),
113746 reverse = oldIndex > newIndex,
113747 animation = this.getLayout().getAnimation(),
113748 tabBar = this.getTabBar(),
113749 oldTab = tabBar.parseActiveTab(oldIndex),
113750 newTab = tabBar.parseActiveTab(newIndex);
113751
113752 if (animation && animation.setReverse) {
113753 animation.setReverse(reverse);
113754 }
113755
113756 this.callParent(arguments);
113757
113758 if (newIndex != -1) {
113759 this.forcedChange = true;
113760 tabBar.setActiveTab(newIndex);
113761 this.forcedChange = false;
113762
113763 if (oldTab) {
113764 oldTab.setActive(false);
113765 }
113766
113767 if (newTab) {
113768 newTab.setActive(true);
113769 }
113770 }
113771 }
113772 },
113773
113774 /**
113775 * Updates this container with the new active item.
113776 * @param {Object} tabBar
113777 * @param {Object} newTab
113778 * @return {Boolean}
113779 */
113780 doTabChange: function(tabBar, newTab) {
113781 var oldActiveItem = this.getActiveItem(),
113782 newActiveItem;
113783
113784 this.setActiveItem(tabBar.indexOf(newTab));
113785 newActiveItem = this.getActiveItem();
113786 return this.forcedChange || oldActiveItem !== newActiveItem;
113787 },
113788
113789 /**
113790 * Creates a new {@link Ext.tab.Bar} instance using {@link Ext#factory}.
113791 * @param {Object} config
113792 * @return {Object}
113793 * @private
113794 */
113795 applyTabBar: function(config) {
113796 if (config === true) {
113797 config = {};
113798 }
113799
113800 if (config) {
113801 Ext.applyIf(config, {
113802 ui: this.getUi(),
113803 docked: this.getTabBarPosition()
113804 });
113805 }
113806
113807 return Ext.factory(config, Ext.tab.Bar, this.getTabBar());
113808 },
113809
113810 /**
113811 * Adds the new {@link Ext.tab.Bar} instance into this container.
113812 * @private
113813 */
113814 updateTabBar: function(newTabBar) {
113815 if (newTabBar) {
113816 this.add(newTabBar);
113817 this.setTabBarPosition(newTabBar.getDocked());
113818 }
113819 },
113820
113821 /**
113822 * Updates the docked position of the {@link #tabBar}.
113823 * @private
113824 */
113825 updateTabBarPosition: function(position) {
113826 var tabBar = this.getTabBar();
113827 if (tabBar) {
113828 tabBar.setDocked(position);
113829 }
113830 },
113831
113832 onItemAdd: function(card) {
113833 var me = this;
113834
113835 if (!card.isInnerItem()) {
113836 return me.callParent(arguments);
113837 }
113838
113839 var tabBar = me.getTabBar(),
113840 initialConfig = card.getInitialConfig(),
113841 tabConfig = initialConfig.tab || {},
113842 tabTitle = (card.getTitle) ? card.getTitle() : initialConfig.title,
113843 tabIconCls = (card.getIconCls) ? card.getIconCls() : initialConfig.iconCls,
113844 tabHidden = (card.getHidden) ? card.getHidden() : initialConfig.hidden,
113845 tabDisabled = (card.getDisabled) ? card.getDisabled() : initialConfig.disabled,
113846 tabBadgeText = (card.getBadgeText) ? card.getBadgeText() : initialConfig.badgeText,
113847 innerItems = me.getInnerItems(),
113848 index = innerItems.indexOf(card),
113849 tabs = tabBar.getItems(),
113850 activeTab = tabBar.getActiveTab(),
113851 currentTabInstance = (tabs.length >= innerItems.length) && tabs.getAt(index),
113852 tabInstance;
113853
113854 if (tabTitle && !tabConfig.title) {
113855 tabConfig.title = tabTitle;
113856 }
113857
113858 if (tabIconCls && !tabConfig.iconCls) {
113859 tabConfig.iconCls = tabIconCls;
113860 }
113861
113862 if (tabHidden && !tabConfig.hidden) {
113863 tabConfig.hidden = tabHidden;
113864 }
113865
113866 if (tabDisabled && !tabConfig.disabled) {
113867 tabConfig.disabled = tabDisabled;
113868 }
113869
113870 if (tabBadgeText && !tabConfig.badgeText) {
113871 tabConfig.badgeText = tabBadgeText;
113872 }
113873
113874 //<debug warn>
113875 if (!currentTabInstance && !tabConfig.title && !tabConfig.iconCls) {
113876 if (!tabConfig.title && !tabConfig.iconCls) {
113877 Ext.Logger.error('Adding a card to a tab container without specifying any tab configuration');
113878 }
113879 }
113880 //</debug>
113881
113882 tabInstance = Ext.factory(tabConfig, Ext.tab.Tab, currentTabInstance);
113883
113884 if (!currentTabInstance) {
113885 tabBar.insert(index, tabInstance);
113886 }
113887
113888 card.tab = tabInstance;
113889
113890 me.callParent(arguments);
113891
113892 if (!activeTab && activeTab !== 0) {
113893 tabBar.setActiveTab(tabBar.getActiveItem());
113894 }
113895 },
113896
113897 /**
113898 * If an item gets enabled/disabled and it has an tab, we should also enable/disable that tab
113899 * @private
113900 */
113901 onItemDisabledChange: function(item, newDisabled) {
113902 if (item && item.tab) {
113903 item.tab.setDisabled(newDisabled);
113904 }
113905 },
113906
113907 // @private
113908 onItemRemove: function(item, index) {
113909 this.getTabBar().remove(item.tab, this.getAutoDestroy());
113910
113911 this.callParent(arguments);
113912 }
113913 }, function() {
113914 });
113915
113916 Ext.define('Ext.table.Cell', {
113917 extend: Ext.Container ,
113918
113919 xtype: 'tablecell',
113920
113921 config: {
113922 baseCls: 'x-table-cell'
113923 },
113924
113925 getElementConfig: function() {
113926 var config = this.callParent();
113927
113928 config.children.length = 0;
113929
113930 return config;
113931 }
113932 });
113933
113934 Ext.define('Ext.table.Row', {
113935 extend: Ext.table.Cell ,
113936
113937 xtype: 'tablerow',
113938
113939 config: {
113940 baseCls: 'x-table-row',
113941 defaultType: 'tablecell'
113942 }
113943 });
113944
113945 Ext.define('Ext.table.Table', {
113946 extend: Ext.Container ,
113947
113948
113949
113950 xtype: 'table',
113951
113952 config: {
113953 baseCls: 'x-table',
113954 defaultType: 'tablerow'
113955 },
113956
113957 cachedConfig: {
113958 fixedLayout: false
113959 },
113960
113961 fixedLayoutCls: 'x-table-fixed',
113962
113963 updateFixedLayout: function(fixedLayout) {
113964 this.innerElement[fixedLayout ? 'addCls' : 'removeCls'](this.fixedLayoutCls);
113965 }
113966 });
113967
113968 /**
113969 *
113970 */
113971 Ext.define('Ext.util.Droppable', {
113972 mixins: {
113973 observable: Ext.mixin.Observable
113974 },
113975
113976 config: {
113977 /**
113978 * The base CSS class to apply to this component's element.
113979 * This will also be prepended to other elements within this component.
113980 */
113981 baseCls: Ext.baseCSSPrefix + 'droppable'
113982 },
113983
113984 /**
113985 * @cfg {String} activeCls
113986 * The CSS added to a Droppable when a Draggable in the same group is being
113987 * dragged.
113988 */
113989 activeCls: Ext.baseCSSPrefix + 'drop-active',
113990
113991 /**
113992 * @cfg {String} invalidCls
113993 * The CSS class to add to the droppable when dragging a draggable that is
113994 * not in the same group.
113995 */
113996 invalidCls: Ext.baseCSSPrefix + 'drop-invalid',
113997
113998 /**
113999 * @cfg {String} hoverCls
114000 * The CSS class to add to the droppable when hovering over a valid drop.
114001 */
114002 hoverCls: Ext.baseCSSPrefix + 'drop-hover',
114003
114004 /**
114005 * @cfg {String} validDropMode
114006 * Determines when a drop is considered 'valid' whether it simply need to
114007 * intersect the region or if it needs to be contained within the region.
114008 * Valid values are: 'intersects' or 'contains'
114009 */
114010 validDropMode: 'intersect',
114011
114012 /**
114013 * @cfg {Boolean} disabled
114014 */
114015 disabled: false,
114016
114017 /**
114018 * @cfg {String} group
114019 * Draggable and Droppable objects can participate in a group which are
114020 * capable of interacting.
114021 */
114022 group: 'base',
114023
114024 // not yet implemented
114025 tolerance: null,
114026
114027 // @private
114028 monitoring: false,
114029
114030 /**
114031 * Creates new Droppable.
114032 * @param {Mixed} el String, HtmlElement or Ext.Element representing an
114033 * element on the page.
114034 * @param {Object} config Configuration options for this class.
114035 */
114036 constructor: function(el, config) {
114037 var me = this;
114038
114039 config = config || {};
114040 Ext.apply(me, config);
114041
114042 /**
114043 * @event dropactivate
114044 * @param {Ext.util.Droppable} this
114045 * @param {Ext.util.Draggable} draggable
114046 * @param {Ext.event.Event} e
114047 */
114048
114049 /**
114050 * @event dropdeactivate
114051 * @param {Ext.util.Droppable} this
114052 * @param {Ext.util.Draggable} draggable
114053 * @param {Ext.event.Event} e
114054 */
114055
114056 /**
114057 * @event dropenter
114058 * @param {Ext.util.Droppable} this
114059 * @param {Ext.util.Draggable} draggable
114060 * @param {Ext.event.Event} e
114061 */
114062
114063 /**
114064 * @event dropleave
114065 * @param {Ext.util.Droppable} this
114066 * @param {Ext.util.Draggable} draggable
114067 * @param {Ext.event.Event} e
114068 */
114069
114070 /**
114071 * @event drop
114072 * @param {Ext.util.Droppable} this
114073 * @param {Ext.util.Draggable} draggable
114074 * @param {Ext.event.Event} e
114075 */
114076
114077 me.el = Ext.get(el);
114078 me.callParent();
114079
114080 me.mixins.observable.constructor.call(me);
114081
114082 if (!me.disabled) {
114083 me.enable();
114084 }
114085
114086 me.el.addCls(me.baseCls);
114087 },
114088
114089 // @private
114090 onDragStart: function(draggable, e) {
114091 if (draggable.group === this.group) {
114092 this.monitoring = true;
114093 this.el.addCls(this.activeCls);
114094 this.region = this.el.getPageBox(true);
114095
114096 draggable.on({
114097 drag: this.onDrag,
114098 beforedragend: this.onBeforeDragEnd,
114099 dragend: this.onDragEnd,
114100 scope: this
114101 });
114102
114103 if (this.isDragOver(draggable)) {
114104 this.setCanDrop(true, draggable, e);
114105 }
114106
114107 this.fireEvent('dropactivate', this, draggable, e);
114108 }
114109 else {
114110 draggable.on({
114111 dragend: function() {
114112 this.el.removeCls(this.invalidCls);
114113 },
114114 scope: this,
114115 single: true
114116 });
114117 this.el.addCls(this.invalidCls);
114118 }
114119 },
114120
114121 // @private
114122 isDragOver: function(draggable, region) {
114123 return this.region[this.validDropMode](draggable.region);
114124 },
114125
114126 // @private
114127 onDrag: function(draggable, e) {
114128 this.setCanDrop(this.isDragOver(draggable), draggable, e);
114129 },
114130
114131 // @private
114132 setCanDrop: function(canDrop, draggable, e) {
114133 if (canDrop && !this.canDrop) {
114134 this.canDrop = true;
114135 this.el.addCls(this.hoverCls);
114136 this.fireEvent('dropenter', this, draggable, e);
114137 }
114138 else if (!canDrop && this.canDrop) {
114139 this.canDrop = false;
114140 this.el.removeCls(this.hoverCls);
114141 this.fireEvent('dropleave', this, draggable, e);
114142 }
114143 },
114144
114145 // @private
114146 onBeforeDragEnd: function(draggable, e) {
114147 draggable.cancelRevert = this.canDrop;
114148 },
114149
114150 // @private
114151 onDragEnd: function(draggable, e) {
114152 this.monitoring = false;
114153 this.el.removeCls(this.activeCls);
114154
114155 draggable.un({
114156 drag: this.onDrag,
114157 beforedragend: this.onBeforeDragEnd,
114158 dragend: this.onDragEnd,
114159 scope: this
114160 });
114161
114162
114163 if (this.canDrop) {
114164 this.canDrop = false;
114165 this.el.removeCls(this.hoverCls);
114166 this.fireEvent('drop', this, draggable, e);
114167 }
114168
114169 this.fireEvent('dropdeactivate', this, draggable, e);
114170 },
114171
114172 /**
114173 * Enable the Droppable target.
114174 * This is invoked immediately after constructing a Droppable if the
114175 * disabled parameter is NOT set to true.
114176 */
114177 enable: function() {
114178 if (!this.mgr) {
114179 this.mgr = Ext.util.Observable.observe(Ext.util.Draggable);
114180 }
114181 this.mgr.on({
114182 dragstart: this.onDragStart,
114183 scope: this
114184 });
114185 this.disabled = false;
114186 },
114187
114188 /**
114189 * Disable the Droppable target.
114190 */
114191 disable: function() {
114192 this.mgr.un({
114193 dragstart: this.onDragStart,
114194 scope: this
114195 });
114196 this.disabled = true;
114197 },
114198
114199 /**
114200 * Method to determine whether this Component is currently disabled.
114201 * @return {Boolean} the disabled state of this Component.
114202 */
114203 isDisabled: function() {
114204 return this.disabled;
114205 },
114206
114207 /**
114208 * Method to determine whether this Droppable is currently monitoring drag operations of Draggables.
114209 * @return {Boolean} the monitoring state of this Droppable
114210 */
114211 isMonitoring: function() {
114212 return this.monitoring;
114213 }
114214 });
114215
114216 /**
114217 * @private
114218 */
114219 Ext.define('Ext.util.TranslatableList', {
114220 extend: Ext.util.translatable.Abstract ,
114221
114222 config: {
114223 items: []
114224 },
114225
114226 applyItems: function(items) {
114227 return Ext.Array.from(items);
114228 },
114229
114230 doTranslate: function(x, y) {
114231 var items = this.getItems(),
114232 offset = 0,
114233 i, ln, item, translateY;
114234
114235 for (i = 0, ln = items.length; i < ln; i++) {
114236 item = items[i];
114237
114238 if (item && !item._list_hidden) {
114239 translateY = y + offset;
114240 offset += item.$height;
114241 item.translate(0, translateY);
114242 }
114243 }
114244 }
114245 });
114246
114247 /**
114248 * @private
114249 */
114250 Ext.define('Ext.util.paintmonitor.OverflowChange', {
114251
114252 extend: Ext.util.paintmonitor.Abstract ,
114253
114254 eventName: Ext.browser.is.Firefox ? 'overflow' : 'overflowchanged',
114255
114256 monitorClass: 'overflowchange',
114257
114258 onElementPainted: function(e) {
114259 this.getCallback().apply(this.getScope(), this.getArgs());
114260 }
114261 });
114262
114263 /**
114264 * @private
114265 * Base class for iOS and Android viewports.
114266 */
114267 Ext.define('Ext.viewport.Default', {
114268 extend: Ext.Container ,
114269
114270 xtype: 'viewport',
114271
114272 PORTRAIT: 'portrait',
114273
114274 LANDSCAPE: 'landscape',
114275
114276
114277
114278
114279
114280
114281
114282 /**
114283 * @event ready
114284 * Fires when the Viewport is in the DOM and ready.
114285 * @param {Ext.Viewport} this
114286 */
114287
114288 /**
114289 * @event maximize
114290 * Fires when the Viewport is maximized.
114291 * @param {Ext.Viewport} this
114292 */
114293
114294 /**
114295 * @event orientationchange
114296 * Fires when the Viewport orientation has changed.
114297 * @param {Ext.Viewport} this
114298 * @param {String} newOrientation The new orientation.
114299 * @param {Number} width The width of the Viewport.
114300 * @param {Number} height The height of the Viewport.
114301 */
114302
114303 config: {
114304 /**
114305 * @private
114306 */
114307 autoMaximize: false,
114308
114309 /**
114310 * @private
114311 */
114312 autoBlurInput: true,
114313
114314 /**
114315 * @cfg {Boolean} preventPanning
114316 * Whether or not to always prevent default panning behavior of the
114317 * browser's viewport.
114318 * @accessor
114319 */
114320 preventPanning: true,
114321
114322 /**
114323 * @cfg {Boolean} preventZooming
114324 * `true` to attempt to stop zooming when you double tap on the screen on mobile devices,
114325 * typically HTC devices with HTC Sense UI.
114326 * @accessor
114327 */
114328 preventZooming: false,
114329
114330 /**
114331 * @cfg
114332 * @private
114333 */
114334 autoRender: true,
114335
114336 /**
114337 * @cfg {Object/String} layout Configuration for this Container's layout. Example:
114338 *
114339 * Ext.create('Ext.Container', {
114340 * layout: {
114341 * type: 'hbox',
114342 * align: 'middle'
114343 * },
114344 * items: [
114345 * {
114346 * xtype: 'panel',
114347 * flex: 1,
114348 * style: 'background-color: red;'
114349 * },
114350 * {
114351 * xtype: 'panel',
114352 * flex: 2,
114353 * style: 'background-color: green'
114354 * }
114355 * ]
114356 * });
114357 *
114358 * See the [layouts guide](../../../core_concepts/layouts.html) for more information.
114359 *
114360 * @accessor
114361 */
114362 layout: 'card',
114363
114364 /**
114365 * @cfg
114366 * @private
114367 */
114368 width: '100%',
114369
114370 /**
114371 * @cfg
114372 * @private
114373 */
114374 height: '100%',
114375
114376 useBodyElement: true,
114377
114378 /**
114379 * An object of all the menus on this viewport.
114380 * @private
114381 */
114382 menus: {},
114383
114384 /**
114385 * @private
114386 */
114387 orientation: null
114388 },
114389
114390 getElementConfig: function() {
114391 var cfg = this.callParent(arguments);
114392
114393 // Used in legacy browser that do not support matchMedia. Hidden element is used for checking of orientation
114394 if (!Ext.feature.has.MatchMedia) {
114395 cfg.children.unshift({reference: 'orientationElement', className: 'x-orientation-inspector'});
114396 }
114397 return cfg;
114398 },
114399
114400 /**
114401 * @property {Boolean} isReady
114402 * `true` if the DOM is ready.
114403 */
114404 isReady: false,
114405
114406 isViewport: true,
114407
114408 isMaximizing: false,
114409
114410 id: 'ext-viewport',
114411
114412 isInputRegex: /^(input|textarea|select|a)$/i,
114413
114414 isInteractiveWebComponentRegEx: /^(audio|video)$/i,
114415
114416 focusedElement: null,
114417
114418 /**
114419 * @private
114420 */
114421 fullscreenItemCls: Ext.baseCSSPrefix + 'fullscreen',
114422
114423 constructor: function(config) {
114424 var bind = Ext.Function.bind;
114425
114426 this.doPreventPanning = bind(this.doPreventPanning, this);
114427 this.doPreventZooming = bind(this.doPreventZooming, this);
114428 this.doBlurInput = bind(this.doBlurInput, this);
114429
114430 this.maximizeOnEvents = [
114431 'ready',
114432 'orientationchange'
114433 ];
114434
114435 // set default devicePixelRatio if it is not explicitly defined
114436 window.devicePixelRatio = window.devicePixelRatio || 1;
114437
114438 this.callSuper([config]);
114439
114440 this.windowWidth = this.getWindowWidth();
114441 this.windowHeight = this.getWindowHeight();
114442 this.windowOuterHeight = this.getWindowOuterHeight();
114443
114444 if (!this.stretchHeights) {
114445 this.stretchHeights = {};
114446 }
114447
114448 if(Ext.feature.has.OrientationChange) {
114449 this.addWindowListener('orientationchange', bind(this.onOrientationChange, this));
114450 } else {
114451 this.addWindowListener('resize', bind(this.onResize, this));
114452 }
114453
114454 document.addEventListener('focus', bind(this.onElementFocus, this), true);
114455 document.addEventListener('blur', bind(this.onElementBlur, this), true);
114456
114457 Ext.onDocumentReady(this.onDomReady, this);
114458
114459 this.on('ready', this.onReady, this, {single: true});
114460 this.getEventDispatcher().addListener('component', '*', 'fullscreen', 'onItemFullscreenChange', this);
114461 return this;
114462 },
114463
114464 onDomReady: function() {
114465 this.isReady = true;
114466 this.updateSize();
114467 this.fireEvent('ready', this);
114468 },
114469
114470 onReady: function() {
114471 if (this.getAutoRender()) {
114472 this.render();
114473 }
114474 if (Ext.browser.name == 'ChromeiOS') {
114475 this.setHeight('-webkit-calc(100% - ' + ((window.outerHeight - window.innerHeight) / 2) + 'px)');
114476 }
114477 },
114478
114479 onElementFocus: function(e) {
114480 this.focusedElement = e.target;
114481 },
114482
114483 onElementBlur: function() {
114484 this.focusedElement = null;
114485 },
114486
114487 render: function() {
114488 if (!this.rendered) {
114489 var body = Ext.getBody(),
114490 clsPrefix = Ext.baseCSSPrefix,
114491 classList = [],
114492 osEnv = Ext.os,
114493 osName = osEnv.name.toLowerCase(),
114494 browserName = Ext.browser.name.toLowerCase(),
114495 osMajorVersion = osEnv.version.getMajor();
114496
114497 this.renderTo(body);
114498
114499 classList.push(clsPrefix + osEnv.deviceType.toLowerCase());
114500
114501 if (osEnv.is.iPad) {
114502 classList.push(clsPrefix + 'ipad');
114503 }
114504
114505 classList.push(clsPrefix + osName);
114506 classList.push(clsPrefix + browserName);
114507
114508 if (osMajorVersion) {
114509 classList.push(clsPrefix + osName + '-' + osMajorVersion);
114510 }
114511
114512 if (osEnv.is.BlackBerry) {
114513 classList.push(clsPrefix + 'bb');
114514 if (Ext.browser.userAgent.match(/Kbd/gi)) {
114515 classList.push(clsPrefix + 'bb-keyboard');
114516 }
114517 }
114518
114519 if (Ext.browser.is.WebKit) {
114520 classList.push(clsPrefix + 'webkit');
114521 }
114522
114523 if (Ext.browser.is.Standalone) {
114524 classList.push(clsPrefix + 'standalone');
114525 }
114526
114527 if (Ext.browser.is.AndroidStock) {
114528 classList.push(clsPrefix + 'android-stock');
114529 }
114530
114531 if (Ext.browser.is.GoogleGlass) {
114532 classList.push(clsPrefix + 'google-glass');
114533 }
114534
114535 this.setOrientation(this.determineOrientation());
114536 classList.push(clsPrefix + this.getOrientation());
114537 body.addCls(classList);
114538 }
114539 },
114540
114541 applyAutoBlurInput: function(autoBlurInput) {
114542 var touchstart = (Ext.feature.has.Touch) ? 'touchstart' : 'mousedown';
114543
114544 if (autoBlurInput) {
114545 this.addWindowListener(touchstart, this.doBlurInput, false);
114546 }
114547 else {
114548 this.removeWindowListener(touchstart, this.doBlurInput, false);
114549 }
114550
114551 return autoBlurInput;
114552 },
114553
114554 applyAutoMaximize: function(autoMaximize) {
114555 if (Ext.browser.is.WebView) {
114556 autoMaximize = false;
114557 }
114558 if (autoMaximize) {
114559 this.on('ready', 'doAutoMaximizeOnReady', this, { single: true });
114560 this.on('orientationchange', 'doAutoMaximizeOnOrientationChange', this);
114561 }
114562 else {
114563 this.un('ready', 'doAutoMaximizeOnReady', this);
114564 this.un('orientationchange', 'doAutoMaximizeOnOrientationChange', this);
114565 }
114566
114567 return autoMaximize;
114568 },
114569
114570 applyPreventPanning: function(preventPanning) {
114571 if (preventPanning) {
114572 this.addWindowListener('touchmove', this.doPreventPanning, false);
114573 }
114574 else {
114575 this.removeWindowListener('touchmove', this.doPreventPanning, false);
114576 }
114577
114578 return preventPanning;
114579 },
114580
114581 applyPreventZooming: function(preventZooming) {
114582 var touchstart = (Ext.feature.has.Touch) ? 'touchstart' : 'mousedown';
114583
114584 if (preventZooming) {
114585 this.addWindowListener(touchstart, this.doPreventZooming, false);
114586 }
114587 else {
114588 this.removeWindowListener(touchstart, this.doPreventZooming, false);
114589 }
114590
114591 return preventZooming;
114592 },
114593
114594 doAutoMaximizeOnReady: function() {
114595 var controller = arguments[arguments.length - 1];
114596
114597 controller.pause();
114598
114599 this.isMaximizing = true;
114600
114601 this.on('maximize', function() {
114602 this.isMaximizing = false;
114603
114604 this.updateSize();
114605
114606 controller.resume();
114607
114608 this.fireEvent('ready', this);
114609 }, this, { single: true });
114610
114611 this.maximize();
114612 },
114613
114614 doAutoMaximizeOnOrientationChange: function() {
114615 var controller = arguments[arguments.length - 1],
114616 firingArguments = controller.firingArguments;
114617
114618 controller.pause();
114619
114620 this.isMaximizing = true;
114621
114622 this.on('maximize', function() {
114623 this.isMaximizing = false;
114624
114625 this.updateSize();
114626
114627 firingArguments[2] = this.windowWidth;
114628 firingArguments[3] = this.windowHeight;
114629
114630 controller.resume();
114631 }, this, { single: true });
114632
114633 this.maximize();
114634 },
114635
114636 doBlurInput: function(e) {
114637 var target = e.target,
114638 focusedElement = this.focusedElement;
114639 //In IE9/10 browser window loses focus and becomes inactive if focused element is <body>. So we shouldn't call blur for <body>
114640 if (focusedElement && focusedElement.nodeName.toUpperCase() != 'BODY' && !this.isInputRegex.test(target.tagName)) {
114641 delete this.focusedElement;
114642 focusedElement.blur();
114643 }
114644 },
114645
114646 doPreventPanning: function(e) {
114647 var target = e.target, touch;
114648
114649 // If we have an interaction on a WebComponent we need to check the actual shadow dom element selected
114650 // to determine if it is an input before preventing default behavior
114651 // Side effect to this is if the shadow input does not do anything with 'touchmove' the user could pan
114652 // the screen.
114653 if (this.isInteractiveWebComponentRegEx.test(target.tagName) && e.touches && e.touches.length > 0) {
114654 touch = e.touches[0];
114655 if (touch && touch.target && this.isInputRegex.test(touch.target.tagName)) {
114656 return;
114657 }
114658 }
114659
114660 if (target && target.nodeType === 1 && !this.isInputRegex.test(target.tagName)) {
114661 e.preventDefault();
114662 }
114663 },
114664
114665 doPreventZooming: function(e) {
114666 // Don't prevent right mouse event
114667 if ('button' in e && e.button !== 0) {
114668 return;
114669 }
114670
114671 var target = e.target, touch;
114672 if (this.isInteractiveWebComponentRegEx.test(target.tagName) && e.touches && e.touches.length > 0) {
114673 touch = e.touches[0];
114674 if (touch && touch.target && this.isInputRegex.test(touch.target.tagName)) {
114675 return;
114676 }
114677 }
114678
114679 if (target && target.nodeType === 1 && !this.isInputRegex.test(target.tagName)) {
114680 e.preventDefault();
114681 }
114682 },
114683
114684 addWindowListener: function(eventName, fn, capturing) {
114685 window.addEventListener(eventName, fn, Boolean(capturing));
114686 },
114687
114688 removeWindowListener: function(eventName, fn, capturing) {
114689 window.removeEventListener(eventName, fn, Boolean(capturing));
114690 },
114691
114692 doAddListener: function(eventName, fn, scope, options) {
114693 if (eventName === 'ready' && this.isReady && !this.isMaximizing) {
114694 fn.call(scope);
114695 return this;
114696 }
114697
114698 return this.callSuper(arguments);
114699 },
114700
114701 determineOrientation: function() {
114702 // First attempt will be to use Native Orientation information
114703 if (Ext.feature.has.Orientation) {
114704 var nativeOrientation= this.getWindowOrientation();
114705 // 90 || -90 || 270 is landscape
114706 if (Math.abs(nativeOrientation) === 90 || nativeOrientation === 270) {
114707 return this.LANDSCAPE;
114708 } else {
114709 return this.PORTRAIT;
114710 }
114711 // Second attempt will be to use MatchMedia and a media query
114712 } else if (Ext.feature.has.MatchMedia) {
114713 return window.matchMedia('(orientation : landscape)').matches ? this.LANDSCAPE : this.PORTRAIT;
114714 // Fall back on hidden element with media query attached to it (media query in Base Theme)
114715 } else if (this.orientationElement) {
114716 return this.orientationElement.getStyle('content');
114717 }
114718 },
114719
114720 updateOrientation: function(newValue, oldValue) {
114721 if (oldValue) {
114722 this.fireOrientationChangeEvent(newValue, oldValue);
114723 }
114724 },
114725
114726 /**
114727 * Listener for Orientation Change in environments that support orientationchange events
114728 * @private
114729 */
114730 onOrientationChange: function() {
114731 this.setOrientation(this.determineOrientation());
114732 },
114733
114734 /**
114735 * Listener for Orientation Change in environments that do no support orientationchange events
114736 * @private
114737 */
114738 onResize: function() {
114739 this.updateSize();
114740 this.setOrientation(this.determineOrientation());
114741 },
114742
114743 fireOrientationChangeEvent: function(newOrientation, oldOrientation) {
114744 var clsPrefix = Ext.baseCSSPrefix;
114745 Ext.getBody().replaceCls(clsPrefix + oldOrientation, clsPrefix + newOrientation);
114746
114747 this.updateSize();
114748 this.fireEvent('orientationchange', this, newOrientation, this.windowWidth, this.windowHeight);
114749 },
114750
114751 updateSize: function(width, height) {
114752 this.windowWidth = width !== undefined ? width : this.getWindowWidth();
114753 this.windowHeight = height !== undefined ? height : this.getWindowHeight();
114754
114755 return this;
114756 },
114757
114758 maximize: function() {
114759 this.fireMaximizeEvent();
114760 },
114761
114762 fireMaximizeEvent: function() {
114763 this.updateSize();
114764 this.fireEvent('maximize', this);
114765 },
114766
114767 doSetHeight: function(height) {
114768 Ext.getBody().setHeight(height);
114769
114770 this.callParent(arguments);
114771 },
114772
114773 doSetWidth: function(width) {
114774 Ext.getBody().setWidth(width);
114775
114776 this.callParent(arguments);
114777 },
114778
114779 scrollToTop: function() {
114780 window.scrollTo(0, -1);
114781 },
114782
114783 /**
114784 * Retrieves the document width.
114785 * @return {Number} width in pixels.
114786 */
114787 getWindowWidth: function() {
114788 return window.innerWidth;
114789 },
114790
114791 /**
114792 * Retrieves the document height.
114793 * @return {Number} height in pixels.
114794 */
114795 getWindowHeight: function() {
114796 return window.innerHeight;
114797 },
114798
114799 getWindowOuterHeight: function() {
114800 return window.outerHeight;
114801 },
114802
114803 getWindowOrientation: function() {
114804 return window.orientation;
114805 },
114806
114807 getSize: function() {
114808 return {
114809 width: this.windowWidth,
114810 height: this.windowHeight
114811 };
114812 },
114813
114814 onItemFullscreenChange: function(item) {
114815 item.addCls(this.fullscreenItemCls);
114816 this.add(item);
114817 },
114818 waitUntil: function(condition, onSatisfied, onTimeout, delay, timeoutDuration) {
114819 if (!delay) {
114820 delay = 50;
114821 }
114822
114823 if (!timeoutDuration) {
114824 timeoutDuration = 2000;
114825 }
114826
114827 var scope = this,
114828 elapse = 0;
114829
114830 setTimeout(function repeat() {
114831 elapse += delay;
114832
114833 if (condition.call(scope) === true) {
114834 if (onSatisfied) {
114835 onSatisfied.call(scope);
114836 }
114837 }
114838 else {
114839 if (elapse >= timeoutDuration) {
114840 if (onTimeout) {
114841 onTimeout.call(scope);
114842 }
114843 }
114844 else {
114845 setTimeout(repeat, delay);
114846 }
114847 }
114848 }, delay);
114849 },
114850
114851 /**
114852 * Sets a menu for a given side of the Viewport.
114853 *
114854 * Adds functionality to show the menu by swiping from the side of the screen from the given side.
114855 *
114856 * If a menu is already set for a given side, it will be removed.
114857 *
114858 * Available sides are: `left`, `right`, `top`, and `bottom`.
114859 *
114860 * @param {Ext.Menu} menu The menu to assign to the viewport
114861 * @param {Object} config The configuration for the menu.
114862 * @param {String} config.side The side to put the menu on.
114863 * @param {Boolean} config.cover True to cover the viewport content. Defaults to `true`.
114864 */
114865 setMenu: function(menu, config) {
114866 var me = this;
114867 config = config || {};
114868
114869 // Temporary workaround for body shifting issue
114870 if (Ext.os.is.iOS && !this.hasiOSOrientationFix) {
114871 this.hasiOSOrientationFix = true;
114872 this.on('orientationchange', function() {
114873 window.scrollTo(0, 0);
114874 }, this);
114875 }
114876
114877 if (!menu) {
114878 //<debug error>
114879 Ext.Logger.error("You must specify a side to dock the menu.");
114880 //</debug>
114881 return;
114882 }
114883
114884 if (!config.side) {
114885 //<debug error>
114886 Ext.Logger.error("You must specify a side to dock the menu.");
114887 //</debug>
114888 return;
114889 }
114890
114891 if (['left', 'right', 'top', 'bottom'].indexOf(config.side) == -1) {
114892 //<debug error>
114893 Ext.Logger.error("You must specify a valid side (left, right, top or botom) to dock the menu.");
114894 //</debug>
114895 return;
114896 }
114897
114898 var menus = me.getMenus();
114899
114900 if (!menus) {
114901 menus = {};
114902 }
114903
114904 // Add a listener to show this menu on swipe
114905 if (!me.addedSwipeListener) {
114906 me.addedSwipeListener = true;
114907
114908 me.element.on({
114909 tap: me.onTap,
114910 swipestart: me.onSwipeStart,
114911 edgeswipestart: me.onEdgeSwipeStart,
114912 edgeswipe: me.onEdgeSwipe,
114913 edgeswipeend: me.onEdgeSwipeEnd,
114914 scope: me
114915 });
114916
114917 // Add BB10 webworks API for swipe down.
114918 if (window.blackberry) {
114919 var toggleMenu = function() {
114920 var menus = me.getMenus(),
114921 menu = menus['top'];
114922
114923 if (!menu) {
114924 return;
114925 }
114926
114927 if (menu.isHidden()) {
114928 me.showMenu('top');
114929 } else {
114930 me.hideMenu('top');
114931 }
114932 };
114933
114934 if (blackberry.app && blackberry.app.event && blackberry.app.event.onSwipeDown) {
114935 blackberry.app.event.onSwipeDown(toggleMenu); // PlayBook
114936 }
114937 else if (blackberry.event && blackberry.event.addEventListener) {
114938 blackberry.event.addEventListener("swipedown", toggleMenu); // BB10
114939 }
114940 }
114941 }
114942
114943 menus[config.side] = menu;
114944 menu.$reveal = Boolean(config.reveal);
114945 menu.$cover = config.cover !== false && !menu.$reveal;
114946 menu.$side = config.side;
114947
114948 me.fixMenuSize(menu, config.side);
114949
114950 if (config.side == 'left') {
114951 menu.setLeft(0);
114952 menu.setRight(null);
114953 menu.setTop(0);
114954 menu.setBottom(0);
114955 }
114956 else if (config.side == 'right') {
114957 menu.setLeft(null);
114958 menu.setRight(0);
114959 menu.setTop(0);
114960 menu.setBottom(0);
114961 }
114962 else if (config.side == 'top') {
114963 menu.setLeft(0);
114964 menu.setRight(0);
114965 menu.setTop(0);
114966 menu.setBottom(null);
114967 }
114968 else if (config.side == 'bottom') {
114969 menu.setLeft(0);
114970 menu.setRight(0);
114971 menu.setTop(null);
114972 menu.setBottom(0);
114973 }
114974
114975 me.setMenus(menus);
114976 },
114977
114978 /**
114979 * Removes a menu from a specified side.
114980 * @param {String} side The side to remove the menu from
114981 */
114982 removeMenu: function(side) {
114983 var menus = this.getMenus() || {},
114984 menu = menus[side];
114985
114986 if(menu) this.hideMenu(side);
114987 delete menus[side];
114988 this.setMenus(menus);
114989 },
114990
114991 /**
114992 * @private
114993 * Changes the sizing of the specified menu so that it displays correctly when shown.
114994 */
114995 fixMenuSize: function(menu, side) {
114996 if (side == 'top' || side == 'bottom') {
114997 menu.setWidth('100%');
114998 }
114999 else if (side == 'left' || side == 'right') {
115000 menu.setHeight('100%');
115001 }
115002 },
115003
115004 /**
115005 * Shows a menu specified by the menu's side.
115006 * @param {String} side The side which the menu is placed.
115007 */
115008 showMenu: function(side) {
115009 var menus = this.getMenus(),
115010 menu = menus[side],
115011 before, after,
115012 viewportBefore, viewportAfter;
115013
115014 if (!menu || menu.isAnimating) {
115015 return;
115016 }
115017
115018 this.hideOtherMenus(side);
115019
115020 before = {
115021 translateX: 0,
115022 translateY: 0
115023 };
115024
115025 after = {
115026 translateX: 0,
115027 translateY: 0
115028 };
115029
115030 viewportBefore = {
115031 translateX: 0,
115032 translateY: 0
115033 };
115034
115035 viewportAfter = {
115036 translateX: 0,
115037 translateY: 0
115038 };
115039
115040 if (menu.$reveal) {
115041 Ext.getBody().insertFirst(menu.element);
115042 }
115043 else {
115044 Ext.Viewport.add(menu);
115045 }
115046
115047 menu.show();
115048 menu.addCls('x-' + side);
115049
115050 var size = (side == 'left' || side == 'right') ? menu.element.getWidth() : menu.element.getHeight();
115051
115052 if (side == 'left') {
115053 before.translateX = -size;
115054 viewportAfter.translateX = size;
115055 }
115056 else if (side == 'right') {
115057 before.translateX = size;
115058 viewportAfter.translateX = -size;
115059 }
115060 else if (side == 'top') {
115061 before.translateY = -size;
115062 viewportAfter.translateY = size;
115063 }
115064 else if (side == 'bottom') {
115065 before.translateY = size;
115066 viewportAfter.translateY = -size;
115067 }
115068
115069 if (menu.$reveal) {
115070 if (Ext.browser.getPreferredTranslationMethod() != 'scrollposition') {
115071 menu.translate(0, 0);
115072 }
115073 }
115074 else {
115075 menu.translate(before.translateX, before.translateY);
115076 }
115077
115078 if (menu.$cover) {
115079 menu.getTranslatable().on('animationend', function() {
115080 menu.isAnimating = false;
115081 }, this, {
115082 single: true
115083 });
115084
115085 menu.translate(after.translateX, after.translateY, {
115086 preserveEndState: true,
115087 duration: 200
115088 });
115089
115090 }
115091 else {
115092 this.translate(viewportBefore.translateX, viewportBefore.translateY);
115093
115094
115095 this.getTranslatable().on('animationend', function() {
115096 menu.isAnimating = false;
115097 }, this, {
115098 single: true
115099 });
115100
115101 this.translate(viewportAfter.translateX, viewportAfter.translateY, {
115102 preserveEndState: true,
115103 duration: 200
115104 });
115105 }
115106
115107 // Make the menu as animating
115108 menu.isAnimating = true;
115109 },
115110
115111 /**
115112 * Hides a menu specified by the menu's side.
115113 * @param {String} side The side which the menu is placed.
115114 */
115115 hideMenu: function(side, animate) {
115116 var menus = this.getMenus(),
115117 menu = menus[side],
115118 after, viewportAfter,
115119 size;
115120
115121 animate = (animate === false) ? false : true;
115122
115123 if (!menu || (menu.isHidden() || menu.isAnimating)) {
115124 return;
115125 }
115126
115127 after = {
115128 translateX: 0,
115129 translateY: 0
115130 };
115131
115132 viewportAfter = {
115133 translateX: 0,
115134 translateY: 0
115135 };
115136
115137 size = (side == 'left' || side == 'right') ? menu.element.getWidth() : menu.element.getHeight();
115138
115139 if (side == 'left') {
115140 after.translateX = -size;
115141 }
115142 else if (side == 'right') {
115143 after.translateX = size;
115144 }
115145 else if (side == 'top') {
115146 after.translateY = -size;
115147 }
115148 else if (side == 'bottom') {
115149 after.translateY = size;
115150 }
115151
115152 if (menu.$cover) {
115153 if (animate) {
115154 menu.getTranslatable().on('animationend', function() {
115155 menu.isAnimating = false;
115156 menu.hide();
115157 }, this, {
115158 single: true
115159 });
115160
115161 menu.translate(after.translateX, after.translateY, {
115162 preserveEndState: true,
115163 duration: 200
115164 });
115165 }
115166 else {
115167 menu.translate(after.translateX, after.translateY);
115168 menu.hide()
115169 }
115170 }
115171 else {
115172 if (animate) {
115173 this.getTranslatable().on('animationend', function() {
115174 menu.isAnimating = false;
115175 menu.hide();
115176 }, this, {
115177 single: true
115178 });
115179
115180 this.translate(viewportAfter.translateX, viewportAfter.translateY, {
115181 preserveEndState: true,
115182 duration: 200
115183 });
115184 }
115185 else {
115186 this.translate(viewportAfter.translateX, viewportAfter.translateY);
115187 menu.hide();
115188 }
115189 }
115190 },
115191
115192 /**
115193 * Hides all visible menus.
115194 */
115195 hideAllMenus: function(animation) {
115196 var menus = this.getMenus();
115197
115198 for (var side in menus) {
115199 this.hideMenu(side, animation);
115200 }
115201 },
115202
115203 /**
115204 * Hides all menus except for the side specified
115205 * @param {String} side Side(s) not to hide
115206 * @param {String} animation Animation to hide with
115207 */
115208 hideOtherMenus: function(side, animation){
115209 var menus = this.getMenus();
115210
115211 for (var menu in menus) {
115212 if (side != menu) {
115213 this.hideMenu(menu, animation);
115214 }
115215 }
115216 },
115217
115218 /**
115219 * Toggles the menu specified by side
115220 * @param {String} side The side which the menu is placed.
115221 */
115222 toggleMenu: function(side) {
115223 var menus = this.getMenus(), menu;
115224 if (menus[side]) {
115225 menu = menus[side];
115226 if (menu.isHidden()) {
115227 this.showMenu(side);
115228 } else {
115229 this.hideMenu(side);
115230 }
115231 }
115232 },
115233
115234 /**
115235 * @private
115236 */
115237 sideForDirection: function(direction) {
115238 if (direction == 'left') {
115239 return 'right';
115240 }
115241 else if (direction == 'right') {
115242 return 'left';
115243 }
115244 else if (direction == 'up') {
115245 return 'bottom';
115246 }
115247 else if (direction == 'down') {
115248 return 'top';
115249 }
115250 },
115251
115252 /**
115253 * @private
115254 */
115255 sideForSwipeDirection: function(direction) {
115256 if (direction == "up") {
115257 return "top";
115258 }
115259 else if (direction == "down") {
115260 return "bottom";
115261 }
115262 return direction;
115263 },
115264
115265 /**
115266 * @private
115267 */
115268 onTap: function(e) {
115269 // this.hideAllMenus();
115270 },
115271
115272 /**
115273 * @private
115274 */
115275 onSwipeStart: function(e) {
115276 var side = this.sideForSwipeDirection(e.direction);
115277 this.hideMenu(side);
115278 },
115279
115280 /**
115281 * @private
115282 */
115283 onEdgeSwipeStart: function(e) {
115284 var side = this.sideForDirection(e.direction),
115285 menus = this.getMenus(),
115286 menu = menus[side],
115287 menuSide, checkMenu;
115288
115289 if (!menu || !menu.isHidden()) {
115290 return;
115291 }
115292
115293 for (menuSide in menus) {
115294 checkMenu = menus[menuSide];
115295 if (checkMenu.isHidden() !== false) {
115296 return;
115297 }
115298 }
115299
115300 this.$swiping = true;
115301
115302 this.hideAllMenus(false);
115303
115304 // show the menu first so we can calculate the size
115305 if (menu.$reveal) {
115306 Ext.getBody().insertFirst(menu.element);
115307 }
115308 else {
115309 Ext.Viewport.add(menu);
115310 }
115311 menu.show();
115312
115313 var size = (side == 'left' || side == 'right') ? menu.element.getWidth() : menu.element.getHeight(),
115314 after, viewportAfter;
115315
115316 after = {
115317 translateX: 0,
115318 translateY: 0
115319 };
115320
115321 viewportAfter = {
115322 translateX: 0,
115323 translateY: 0
115324 };
115325
115326 if (side == 'left') {
115327 after.translateX = -size;
115328 }
115329 else if (side == 'right') {
115330 after.translateX = size;
115331 }
115332 else if (side == 'top') {
115333 after.translateY = -size;
115334 }
115335 else if (side == 'bottom') {
115336 after.translateY = size;
115337 }
115338
115339 var transformStyleName = 'webkitTransform' in document.createElement('div').style ? 'webkitTransform' : 'transform',
115340 setTransform = menu.element.dom.style[transformStyleName];
115341
115342 if (setTransform) {
115343 menu.element.dom.style[transformStyleName] = '';
115344 }
115345
115346 if (menu.$reveal) {
115347 if (Ext.browser.getPreferredTranslationMethod() != 'scrollposition') {
115348 menu.translate(0, 0);
115349 }
115350 }
115351 else {
115352 menu.translate(after.translateX, after.translateY);
115353 }
115354
115355 if (!menu.$cover) {
115356 if (setTransform) {
115357 this.innerElement.dom.style[transformStyleName] = '';
115358 }
115359
115360 this.translate(viewportAfter.translateX, viewportAfter.translateY);
115361 }
115362 },
115363
115364 /**
115365 * @private
115366 */
115367 onEdgeSwipe: function(e) {
115368 var side = this.sideForDirection(e.direction),
115369 menu = this.getMenus()[side];
115370
115371 if (!menu || !this.$swiping) {
115372 return;
115373 }
115374
115375 var size = (side == 'left' || side == 'right') ? menu.element.getWidth() : menu.element.getHeight(),
115376 after, viewportAfter,
115377 movement = Math.min(e.distance - size, 0),
115378 viewportMovement = Math.min(e.distance, size);
115379
115380 after = {
115381 translateX: 0,
115382 translateY: 0
115383 };
115384
115385 viewportAfter = {
115386 translateX: 0,
115387 translateY: 0
115388 };
115389
115390 if (side == 'left') {
115391 after.translateX = movement;
115392 viewportAfter.translateX = viewportMovement;
115393 }
115394 else if (side == 'right') {
115395 after.translateX = -movement;
115396 viewportAfter.translateX = -viewportMovement;
115397 }
115398 else if (side == 'top') {
115399 after.translateY = movement;
115400 viewportAfter.translateY = viewportMovement;
115401 }
115402 else if (side == 'bottom') {
115403 after.translateY = -movement;
115404 viewportAfter.translateY = -viewportMovement;
115405 }
115406
115407 if (menu.$cover) {
115408 menu.translate(after.translateX, after.translateY);
115409 }
115410 else {
115411 this.translate(viewportAfter.translateX, viewportAfter.translateY);
115412 }
115413 },
115414
115415 /**
115416 * @private
115417 */
115418 onEdgeSwipeEnd: function(e) {
115419 var side = this.sideForDirection(e.direction),
115420 menu = this.getMenus()[side],
115421 shouldRevert = false;
115422
115423 if (!menu) {
115424 return;
115425 }
115426
115427 var size = (side == 'left' || side == 'right') ? menu.element.getWidth() : menu.element.getHeight(),
115428 velocity = (e.flick) ? e.flick.velocity : 0;
115429
115430 // check if continuing in the right direction
115431 if (side == 'right') {
115432 if (velocity.x > 0) {
115433 shouldRevert = true;
115434 }
115435 }
115436 else if (side == 'left') {
115437 if (velocity.x < 0) {
115438 shouldRevert = true;
115439 }
115440 }
115441 else if (side == 'top') {
115442 if (velocity.y < 0) {
115443 shouldRevert = true;
115444 }
115445 }
115446 else if (side == 'bottom') {
115447 if (velocity.y > 0) {
115448 shouldRevert = true;
115449 }
115450 }
115451
115452 var movement = (shouldRevert) ? size : 0,
115453 viewportMovement = (shouldRevert) ? 0 : -size,
115454 after, viewportAfter;
115455
115456 after = {
115457 translateX: 0,
115458 translateY: 0
115459 };
115460
115461 viewportAfter = {
115462 translateX: 0,
115463 translateY: 0
115464 };
115465
115466 if (side == 'left') {
115467 after.translateX = -movement;
115468 viewportAfter.translateX = -viewportMovement;
115469 }
115470 else if (side == 'right') {
115471 after.translateX = movement;
115472 viewportAfter.translateX = viewportMovement;
115473 }
115474 else if (side == 'top') {
115475 after.translateY = -movement;
115476 viewportAfter.translateY = -viewportMovement;
115477 }
115478 else if (side == 'bottom') {
115479 after.translateY = movement;
115480 viewportAfter.translateY = viewportMovement;
115481 }
115482
115483 // Move the viewport if cover is not enabled
115484 if (menu.$cover) {
115485 menu.getTranslatable().on('animationend', function() {
115486 if (shouldRevert) {
115487 menu.hide();
115488 }
115489 }, this, {
115490 single: true
115491 });
115492
115493 menu.translate(after.translateX, after.translateY, {
115494 preserveEndState: true,
115495 duration: 200
115496 });
115497
115498 }
115499 else {
115500 this.getTranslatable().on('animationend', function() {
115501 if (shouldRevert) {
115502 menu.hide();
115503 }
115504 }, this, {
115505 single: true
115506 });
115507
115508 this.translate(viewportAfter.translateX, viewportAfter.translateY, {
115509 preserveEndState: true,
115510 duration: 200
115511 });
115512 }
115513
115514 this.$swiping = false;
115515 }
115516 });
115517
115518 /**
115519 * @private
115520 * Android version of viewport.
115521 */
115522 Ext.define('Ext.viewport.AndroidStock', {
115523 extend: Ext.viewport.Default ,
115524 alternateClassName: ['Ext.viewport.Android'],
115525
115526 config: {
115527 translatable: {
115528 translationMethod: 'csstransform'
115529 }
115530 },
115531
115532 constructor: function() {
115533 this.on('orientationchange', 'hideKeyboardIfNeeded', this, { prepend: true });
115534
115535 this.callSuper(arguments);
115536 },
115537
115538 getWindowWidth: function () {
115539 return this.element.getWidth();
115540
115541 },
115542
115543 getWindowHeight: function () {
115544 return this.element.getHeight();
115545 },
115546
115547 getDummyInput: function() {
115548 var input = this.dummyInput,
115549 focusedElement = this.focusedElement,
115550 box = Ext.fly(focusedElement).getPageBox();
115551
115552 if (!input) {
115553 this.dummyInput = input = document.createElement('input');
115554 input.style.position = 'absolute';
115555 input.style.opacity = '0';
115556 input.style.pointerEvents = 'none';
115557 document.body.appendChild(input);
115558 }
115559
115560 input.style.left = box.left + 'px';
115561 input.style.top = box.top + 'px';
115562 input.style.display = '';
115563
115564 return input;
115565 },
115566
115567 doBlurInput: function(e) {
115568 var target = e.target,
115569 focusedElement = this.focusedElement,
115570 dummy;
115571
115572 if (focusedElement && !this.isInputRegex.test(target.tagName)) {
115573 dummy = this.getDummyInput();
115574 delete this.focusedElement;
115575 dummy.focus();
115576
115577 setTimeout(function() {
115578 dummy.style.display = 'none';
115579 }, 100);
115580 }
115581 },
115582
115583 hideKeyboardIfNeeded: function() {
115584 var eventController = arguments[arguments.length - 1],
115585 focusedElement = this.focusedElement;
115586
115587 if (focusedElement) {
115588 delete this.focusedElement;
115589 eventController.pause();
115590
115591 if (Ext.os.version.lt('4')) {
115592 focusedElement.style.display = 'none';
115593 }
115594 else {
115595 focusedElement.blur();
115596 }
115597
115598 setTimeout(function() {
115599 focusedElement.style.display = '';
115600 eventController.resume();
115601 }, 1000);
115602 }
115603 },
115604
115605 getActualWindowOuterHeight: function() {
115606 return Math.round(this.getWindowOuterHeight() / window.devicePixelRatio);
115607 },
115608
115609 maximize: function() {
115610 var stretchHeights = this.stretchHeights,
115611 orientation = this.getOrientation(),
115612 height;
115613
115614 height = stretchHeights[orientation];
115615
115616 if (!height) {
115617 stretchHeights[orientation] = height = this.getActualWindowOuterHeight();
115618 }
115619
115620 if (!this.addressBarHeight) {
115621 this.addressBarHeight = height - this.getWindowHeight();
115622 }
115623
115624 this.setHeight(height);
115625
115626 var isHeightMaximized = Ext.Function.bind(this.isHeightMaximized, this, [height]);
115627
115628 this.scrollToTop();
115629 this.waitUntil(isHeightMaximized, this.fireMaximizeEvent, this.fireMaximizeEvent);
115630 },
115631
115632 isHeightMaximized: function(height) {
115633 this.scrollToTop();
115634 return this.getWindowHeight() === height;
115635 },
115636
115637 doPreventZooming: function (e) {
115638 // Don't prevent right mouse event
115639 if ('button' in e && e.button !== 0) {
115640 return;
115641 }
115642
115643 var target = e.target;
115644
115645 if (target && target.nodeType === 1 && !this.isInputRegex.test(target.tagName) && !this.focusedElement) {
115646 e.preventDefault();
115647 }
115648 }
115649
115650 }, function() {
115651 if (!Ext.os.is.Android) {
115652 return;
115653 }
115654
115655 var version = Ext.os.version,
115656 userAgent = Ext.browser.userAgent,
115657 // These Android devices have a nasty bug which causes JavaScript timers to be completely frozen
115658 // when the browser's viewport is being panned.
115659 isBuggy = /(htc|desire|incredible|ADR6300)/i.test(userAgent) && version.lt('2.3');
115660
115661 if (isBuggy) {
115662 this.override({
115663 constructor: function(config) {
115664 if (!config) {
115665 config = {};
115666 }
115667
115668 config.autoMaximize = false;
115669
115670 this.watchDogTick = Ext.Function.bind(this.watchDogTick, this);
115671
115672 setInterval(this.watchDogTick, 1000);
115673
115674 return this.callParent([config]);
115675 },
115676
115677 watchDogTick: function() {
115678 this.watchDogLastTick = Ext.Date.now();
115679 },
115680
115681 doPreventPanning: function() {
115682 var now = Ext.Date.now(),
115683 lastTick = this.watchDogLastTick,
115684 deltaTime = now - lastTick;
115685
115686 // Timers are frozen
115687 if (deltaTime >= 2000) {
115688 return;
115689 }
115690
115691 return this.callParent(arguments);
115692 },
115693
115694 doPreventZooming: function() {
115695 var now = Ext.Date.now(),
115696 lastTick = this.watchDogLastTick,
115697 deltaTime = now - lastTick;
115698
115699 // Timers are frozen
115700 if (deltaTime >= 2000) {
115701 return;
115702 }
115703
115704 return this.callParent(arguments);
115705 }
115706 });
115707 }
115708
115709 if (version.match('2')) {
115710 this.override({
115711 onReady: function() {
115712 this.addWindowListener('resize', Ext.Function.bind(this.onWindowResize, this));
115713
115714 this.callParent(arguments);
115715 },
115716
115717 scrollToTop: function() {
115718 document.body.scrollTop = 100;
115719 },
115720
115721 onWindowResize: function() {
115722 var oldWidth = this.windowWidth,
115723 oldHeight = this.windowHeight,
115724 width = this.getWindowWidth(),
115725 height = this.getWindowHeight();
115726
115727 if (this.getAutoMaximize() && !this.isMaximizing && !this.orientationChanging
115728 && window.scrollY === 0
115729 && oldWidth === width
115730 && height < oldHeight
115731 && ((height >= oldHeight - this.addressBarHeight) || !this.focusedElement)) {
115732 this.scrollToTop();
115733 }
115734 }
115735 });
115736 }
115737 else if (version.gtEq('3.1')) {
115738 this.override({
115739 isHeightMaximized: function(height) {
115740 this.scrollToTop();
115741 return this.getWindowHeight() === height - 1;
115742 }
115743 });
115744 }
115745 else if (version.match('3')) {
115746 this.override({
115747 isHeightMaximized: function() {
115748 this.scrollToTop();
115749 return true;
115750 }
115751 })
115752 }
115753
115754 if (version.gtEq('4')) {
115755 this.override({
115756 doBlurInput: Ext.emptyFn
115757 });
115758 }
115759 });
115760
115761 /**
115762 * @private
115763 * iOS version of viewport.
115764 */
115765 Ext.define('Ext.viewport.Ios', {
115766 extend: Ext.viewport.Default ,
115767
115768 isFullscreen: function() {
115769 return this.isHomeScreen();
115770 },
115771
115772 isHomeScreen: function() {
115773 return window.navigator.standalone === true;
115774 },
115775
115776 constructor: function() {
115777 this.callParent(arguments);
115778
115779 if (this.getAutoMaximize() && !this.isFullscreen()) {
115780 this.addWindowListener('touchstart', Ext.Function.bind(this.onTouchStart, this));
115781 }
115782 },
115783
115784 maximize: function() {
115785 if (this.isFullscreen()) {
115786 return this.callParent();
115787 }
115788
115789 var stretchHeights = this.stretchHeights,
115790 orientation = this.getOrientation(),
115791 currentHeight = this.getWindowHeight(),
115792 height = stretchHeights[orientation];
115793
115794 if (window.scrollY > 0) {
115795 this.scrollToTop();
115796
115797 if (!height) {
115798 stretchHeights[orientation] = height = this.getWindowHeight();
115799 }
115800
115801 this.setHeight(height);
115802 this.fireMaximizeEvent();
115803 }
115804 else {
115805 if (!height) {
115806 height = this.getScreenHeight();
115807 }
115808
115809 this.setHeight(height);
115810
115811 this.waitUntil(function() {
115812 this.scrollToTop();
115813 return currentHeight !== this.getWindowHeight();
115814 }, function() {
115815 if (!stretchHeights[orientation]) {
115816 height = stretchHeights[orientation] = this.getWindowHeight();
115817 this.setHeight(height);
115818 }
115819
115820 this.fireMaximizeEvent();
115821 }, function() {
115822 //<debug error>
115823 Ext.Logger.error("Timeout waiting for window.innerHeight to change", this);
115824 //</debug>
115825 height = stretchHeights[orientation] = this.getWindowHeight();
115826 this.setHeight(height);
115827 this.fireMaximizeEvent();
115828 }, 50, 1000);
115829 }
115830 },
115831
115832 getScreenHeight: function() {
115833 var orientation = this.getOrientation();
115834 return window.screen[orientation === this.PORTRAIT ? 'height' : 'width'];
115835 },
115836
115837 onElementFocus: function() {
115838 if (this.getAutoMaximize() && !this.isFullscreen()) {
115839 clearTimeout(this.scrollToTopTimer);
115840 }
115841
115842 this.callParent(arguments);
115843 },
115844
115845 onElementBlur: function() {
115846 if (this.getAutoMaximize() && !this.isFullscreen()) {
115847 this.scrollToTopTimer = setTimeout(this.scrollToTop, 500);
115848 }
115849
115850 this.callParent(arguments);
115851 },
115852
115853 onTouchStart: function() {
115854 if (this.focusedElement === null) {
115855 this.scrollToTop();
115856 }
115857 },
115858
115859 scrollToTop: function() {
115860 window.scrollTo(0, 0);
115861 }
115862
115863 }, function() {
115864 if (!Ext.os.is.iOS) {
115865 return;
115866 }
115867
115868 if (Ext.os.version.lt('3.2')) {
115869 this.override({
115870 constructor: function() {
115871 var stretchHeights = this.stretchHeights = {};
115872
115873 stretchHeights[this.PORTRAIT] = 416;
115874 stretchHeights[this.LANDSCAPE] = 268;
115875
115876 return this.callOverridden(arguments);
115877 }
115878 });
115879 }
115880
115881 if (Ext.os.version.lt('5')) {
115882 this.override({
115883 fieldMaskClsTest: '-field-mask',
115884
115885 doPreventZooming: function(e) {
115886 var target = e.target;
115887
115888 if (target && target.nodeType === 1 && !this.isInputRegex.test(target.tagName) &&
115889 target.className.indexOf(this.fieldMaskClsTest) == -1) {
115890 e.preventDefault();
115891 }
115892 }
115893 });
115894 }
115895
115896 if (Ext.os.is.iPad) {
115897 this.override({
115898 isFullscreen: function() {
115899 return true;
115900 }
115901 });
115902 }
115903
115904 if (Ext.os.version.gtEq('7')) {
115905 // iPad or Homescreen or UIWebView
115906 if (Ext.os.deviceType === 'Tablet' || !Ext.browser.is.Safari || window.navigator.standalone) {
115907 this.override({
115908 constructor: function() {
115909 var stretchHeights = {},
115910 stretchWidths = {},
115911 orientation = this.determineOrientation(),
115912 screenHeight = window.screen.height,
115913 screenWidth = window.screen.width,
115914 menuHeight = orientation === this.PORTRAIT
115915 ? screenHeight - window.innerHeight
115916 : screenWidth - window.innerHeight;
115917
115918 stretchHeights[this.PORTRAIT] = screenHeight - menuHeight;
115919 stretchHeights[this.LANDSCAPE] = screenWidth - menuHeight;
115920
115921 stretchWidths[this.PORTRAIT] = screenWidth;
115922 stretchWidths[this.LANDSCAPE] = screenHeight;
115923
115924 this.stretchHeights = stretchHeights;
115925 this.stretchWidths = stretchWidths;
115926
115927 this.callOverridden(arguments);
115928
115929 this.on('ready', this.setViewportSizeToAbsolute, this);
115930 this.on('orientationchange', this.setViewportSizeToAbsolute, this);
115931 },
115932
115933 getWindowHeight: function() {
115934 var orientation = this.getOrientation();
115935 return this.stretchHeights[orientation];
115936 },
115937
115938 getWindowWidth: function() {
115939 var orientation = this.getOrientation();
115940 return this.stretchWidths[orientation];
115941 },
115942
115943 setViewportSizeToAbsolute: function() {
115944 this.setWidth(this.getWindowWidth());
115945 this.setHeight(this.getWindowHeight());
115946 }
115947 });
115948 }
115949
115950 // iPad Only
115951 if (Ext.os.deviceType === 'Tablet') {
115952 this.override({
115953 constructor: function() {
115954 this.callOverridden(arguments);
115955
115956 window.addEventListener('scroll', function() {
115957 if (window.scrollX !== 0) {
115958 window.scrollTo(0, window.scrollY);
115959 }
115960 }, false);
115961 },
115962
115963 setViewportSizeToAbsolute: function() {
115964 window.scrollTo(0, 0);
115965
115966 this.callOverridden(arguments);
115967 },
115968
115969 onElementBlur: function() {
115970 this.callOverridden(arguments);
115971 if (window.scrollY !== 0) {
115972 window.scrollTo(0, 0);
115973 }
115974 }
115975 });
115976 }
115977 }
115978 });
115979
115980 /**
115981 * @private
115982 * Windows Phone version of Viewport.
115983 */
115984 Ext.define('Ext.viewport.WindowsPhone', {
115985 requires: [],
115986
115987 alternateClassName: 'Ext.viewport.WP',
115988
115989 extend: Ext.viewport.Default ,
115990
115991 // so one pixel line is displayed on the right side of the screen. Setting width more than 100% fix the issue
115992 // config: {
115993 // width: '100.2%',
115994 // height: '100.2%'
115995 // },
115996
115997 config: {
115998 translatable: {
115999 translationMethod: 'csstransform'
116000 }
116001 },
116002
116003 initialize: function () {
116004 // There is -ms-user-select CSS property for IE10, but it seems it works only in desktop browser. So we need to prevent selection event.
116005 var preventSelection = function(e) {
116006 var srcElement = e.srcElement.nodeName.toUpperCase(),
116007 selectableElements = ['INPUT', 'TEXTAREA'];
116008
116009 if (selectableElements.indexOf(srcElement) == -1) {
116010 return false;
116011 }
116012 };
116013
116014 document.body.addEventListener('onselectstart', preventSelection);
116015
116016 this.callParent(arguments);
116017 }
116018 });
116019
116020 /**
116021 * This class acts as a factory for environment-specific viewport implementations.
116022 *
116023 * Please refer to the {@link Ext.Viewport} documentation about using the global instance.
116024 * @private
116025 */
116026 Ext.define('Ext.viewport.Viewport', {
116027
116028
116029
116030
116031
116032
116033 constructor: function(config) {
116034 var osName = Ext.os.name,
116035 viewportName, viewport;
116036
116037 switch (osName) {
116038 case 'Android':
116039 viewportName = (Ext.browser.name == 'ChromeMobile') ? 'Default' : 'AndroidStock';
116040 break;
116041
116042 case 'iOS':
116043 viewportName = 'Ios';
116044 break;
116045
116046 case 'Windows':
116047 viewportName = (Ext.browser.name == 'IE') ? 'WindowsPhone' : 'Default';
116048 break;
116049
116050 case 'WindowsPhone':
116051 viewportName = 'WindowsPhone';
116052 break;
116053
116054 default:
116055 viewportName = 'Default';
116056 break;
116057 }
116058
116059 viewport = Ext.create('Ext.viewport.' + viewportName, config);
116060
116061 return viewport;
116062 }
116063 });
116064
116065 // Docs for the singleton instance created by above factory:
116066
116067 /**
116068 * @class Ext.Viewport
116069 * @extends Ext.viewport.Default
116070 * @singleton
116071 *
116072 * Ext.Viewport is a instance created when you use {@link Ext#setup}. Because {@link Ext.Viewport} extends from
116073 * {@link Ext.Container}, it has as {@link #layout} (which defaults to {@link Ext.layout.Card}). This means you
116074 * can add items to it at any time, from anywhere in your code. The {@link Ext.Viewport} {@link #cfg-fullscreen}
116075 * configuration is `true` by default, so it will take up your whole screen.
116076 *
116077 * @example raw
116078 * Ext.setup({
116079 * onReady: function() {
116080 * Ext.Viewport.add({
116081 * xtype: 'container',
116082 * html: 'My new container!'
116083 * });
116084 * }
116085 * });
116086 *
116087 * If you want to customize anything about this {@link Ext.Viewport} instance, you can do so by adding a property
116088 * called `viewport` into your {@link Ext#setup} object:
116089 *
116090 * @example raw
116091 * Ext.setup({
116092 * viewport: {
116093 * layout: 'vbox'
116094 * },
116095 * onReady: function() {
116096 * //do something
116097 * }
116098 * });
116099 *
116100 * **Note** if you use {@link Ext#onReady}, this instance of {@link Ext.Viewport} will **not** be created. Though, in most cases,
116101 * you should **not** use {@link Ext#onReady}.
116102 */
116103